diff --git a/internal/api/grpc/auth/permission.go b/internal/api/grpc/auth/permission.go index d1fa1c356c..fe6fe47313 100644 --- a/internal/api/grpc/auth/permission.go +++ b/internal/api/grpc/auth/permission.go @@ -29,18 +29,18 @@ func (s *Server) ListMyProjectPermissions(ctx context.Context, _ *auth_pb.ListMy } func (s *Server) ListMyMemberships(ctx context.Context, req *auth_pb.ListMyMembershipsRequest) (*auth_pb.ListMyMembershipsResponse, error) { - request, err := ListMyMembershipsRequestToModel(req) + request, err := ListMyMembershipsRequestToModel(ctx, req) if err != nil { return nil, err } - response, err := s.repo.SearchMyUserMemberships(ctx, request) + response, err := s.query.Memberships(ctx, request) if err != nil { return nil, err } return &auth_pb.ListMyMembershipsResponse{ - Result: user_grpc.MembershipsToMembershipsPb(response.Result), + Result: user_grpc.MembershipsToMembershipsPb(response.Memberships), Details: obj_grpc.ToListDetails( - response.TotalResult, + response.Count, response.Sequence, response.Timestamp, ), diff --git a/internal/api/grpc/auth/permission_converter.go b/internal/api/grpc/auth/permission_converter.go index 5d3b6f73a2..793a07d326 100644 --- a/internal/api/grpc/auth/permission_converter.go +++ b/internal/api/grpc/auth/permission_converter.go @@ -1,23 +1,33 @@ package auth import ( + "context" + + "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/grpc/object" user_grpc "github.com/caos/zitadel/internal/api/grpc/user" - user_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/query" auth_pb "github.com/caos/zitadel/pkg/grpc/auth" ) -func ListMyMembershipsRequestToModel(req *auth_pb.ListMyMembershipsRequest) (*user_model.UserMembershipSearchRequest, error) { +func ListMyMembershipsRequestToModel(ctx context.Context, req *auth_pb.ListMyMembershipsRequest) (*query.MembershipSearchQuery, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - queries, err := user_grpc.MembershipQueriesToModel(req.Queries) + queries, err := user_grpc.MembershipQueriesToQuery(req.Queries) if err != nil { return nil, err } - return &user_model.UserMembershipSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - //SortingColumn: //TODO: sorting + userQuery, err := query.NewMembershipUserIDQuery(authz.GetCtxData(ctx).UserID) + if err != nil { + return nil, err + } + queries = append(queries, userQuery) + return &query.MembershipSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + //SortingColumn: //TODO: sorting + }, Queries: queries, }, nil } diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 0c19e9b136..e0c978da0d 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -7,7 +7,6 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/grpc/change" "github.com/caos/zitadel/internal/api/grpc/metadata" - "github.com/caos/zitadel/internal/api/grpc/object" obj_grpc "github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/api/grpc/org" user_grpc "github.com/caos/zitadel/internal/api/grpc/user" @@ -102,7 +101,7 @@ func (s *Server) UpdateMyUserName(ctx context.Context, req *auth_pb.UpdateMyUser return nil, err } return &auth_pb.UpdateMyUserNameResponse{ - Details: object.DomainToChangeDetailsPb(objectDetails), + Details: obj_grpc.DomainToChangeDetailsPb(objectDetails), }, nil } @@ -121,7 +120,7 @@ func (s *Server) ListMyUserGrants(ctx context.Context, req *auth_pb.ListMyUserGr } return &auth_pb.ListMyUserGrantsResponse{ Result: UserGrantsToPb(res.Result), - Details: object.ToListDetails( + Details: obj_grpc.ToListDetails( res.TotalResult, res.Sequence, res.Timestamp, @@ -140,13 +139,13 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje } return &auth_pb.ListMyProjectOrgsResponse{ //TODO: not all details - Details: object.ToListDetails(res.TotalResult, 0, time.Time{}), + Details: obj_grpc.ToListDetails(res.TotalResult, 0, time.Time{}), Result: org.OrgsToPb(res.Result), }, nil } func ListMyProjectOrgsRequestToModel(req *auth_pb.ListMyProjectOrgsRequest) (*grant_model.UserGrantSearchRequest, error) { - offset, limit, asc := object.ListQueryToModel(req.Query) + offset, limit, asc := obj_grpc.ListQueryToModel(req.Query) queries, err := org.OrgQueriesToUserGrantModel(req.Queries) if err != nil { return nil, err diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 516dc08ab6..f099740428 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -634,18 +634,18 @@ func (s *Server) RemoveHumanLinkedIDP(ctx context.Context, req *mgmt_pb.RemoveHu } func (s *Server) ListUserMemberships(ctx context.Context, req *mgmt_pb.ListUserMembershipsRequest) (*mgmt_pb.ListUserMembershipsResponse, error) { - request, err := ListUserMembershipsRequestToModel(req) + request, err := ListUserMembershipsRequestToModel(ctx, req) if err != nil { return nil, err } - response, err := s.user.SearchUserMemberships(ctx, request) + response, err := s.query.Memberships(ctx, request) if err != nil { return nil, err } return &mgmt_pb.ListUserMembershipsResponse{ - Result: user_grpc.MembershipsToMembershipsPb(response.Result), + Result: user_grpc.MembershipsToMembershipsPb(response.Memberships), Details: obj_grpc.ToListDetails( - response.TotalResult, + response.Count, response.Sequence, response.Timestamp, ), diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 1f3f1b6938..99bfe0f379 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -255,21 +255,27 @@ func ListHumanLinkedIDPsRequestToQuery(ctx context.Context, req *mgmt_pb.ListHum }, nil } -func ListUserMembershipsRequestToModel(req *mgmt_pb.ListUserMembershipsRequest) (*user_model.UserMembershipSearchRequest, error) { +func ListUserMembershipsRequestToModel(ctx context.Context, req *mgmt_pb.ListUserMembershipsRequest) (*query.MembershipSearchQuery, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - queries, err := user_grpc.MembershipQueriesToModel(req.Queries) + queries, err := user_grpc.MembershipQueriesToQuery(req.Queries) if err != nil { return nil, err } - queries = append(queries, &user_model.UserMembershipSearchQuery{ - Key: user_model.UserMembershipSearchKeyUserID, - Method: domain.SearchMethodEquals, - Value: req.UserId, - }) - return &user_model.UserMembershipSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, + userQuery, err := query.NewMembershipUserIDQuery(req.UserId) + if err != nil { + return nil, err + } + ownerQuery, err := query.NewMembershipResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + queries = append(queries, userQuery, ownerQuery) + return &query.MembershipSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, //SortingColumn: //TODO: sorting Queries: queries, }, nil diff --git a/internal/api/grpc/object/converter.go b/internal/api/grpc/object/converter.go index fc91963bab..05fc4453a8 100644 --- a/internal/api/grpc/object/converter.go +++ b/internal/api/grpc/object/converter.go @@ -124,7 +124,7 @@ func TextMethodToQuery(method object_pb.TextQueryMethod) query.TextComparison { func ListQueryToModel(query *object_pb.ListQuery) (offset, limit uint64, asc bool) { if query == nil { - return + return 0, 0, false } return query.Offset, uint64(query.Limit), query.Asc } diff --git a/internal/api/grpc/user/membership.go b/internal/api/grpc/user/membership.go index 1ed46cf3bf..ccc66c4ed3 100644 --- a/internal/api/grpc/user/membership.go +++ b/internal/api/grpc/user/membership.go @@ -4,34 +4,35 @@ 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" user_model "github.com/caos/zitadel/internal/user/model" user_pb "github.com/caos/zitadel/pkg/grpc/user" ) -func MembershipQueriesToModel(queries []*user_pb.MembershipQuery) (_ []*user_model.UserMembershipSearchQuery, err error) { - q := make([]*user_model.UserMembershipSearchQuery, 0) +func MembershipQueriesToQuery(queries []*user_pb.MembershipQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, 0) for _, query := range queries { - qs, err := MembershipQueryToModel(query) + qs, err := MembershipQueryToQuery(query) if err != nil { return nil, err } - q = append(q, qs...) + q = append(q, qs) } return q, nil } -func MembershipQueryToModel(query *user_pb.MembershipQuery) ([]*user_model.UserMembershipSearchQuery, error) { - switch q := query.Query.(type) { +func MembershipQueryToQuery(req *user_pb.MembershipQuery) (query.SearchQuery, error) { + switch q := req.Query.(type) { case *user_pb.MembershipQuery_OrgQuery: - return MembershipOrgQueryToModel(q.OrgQuery), nil + return query.NewMembershipOrgIDQuery(q.OrgQuery.OrgId) case *user_pb.MembershipQuery_ProjectQuery: - return MembershipProjectQueryToModel(q.ProjectQuery), nil + return query.NewMembershipProjectIDQuery(q.ProjectQuery.ProjectId) case *user_pb.MembershipQuery_ProjectGrantQuery: - return MembershipProjectGrantQueryToModel(q.ProjectGrantQuery), nil + return query.NewMembershipProjectGrantIDQuery(q.ProjectGrantQuery.ProjectGrantId) case *user_pb.MembershipQuery_IamQuery: - return MembershipIAMQueryToModel(q.IamQuery), nil + return query.NewMembershipIsIAMQuery() default: - return nil, errors.ThrowInvalidArgument(nil, "USER-dsg3z", "List.Query.Invalid") + return nil, errors.ThrowInvalidArgument(nil, "USER-dsg3z", "Errors.List.Query.Invalid") } } @@ -91,7 +92,7 @@ func MembershipProjectGrantQueryToModel(q *user_pb.MembershipProjectGrantQuery) } } -func MembershipsToMembershipsPb(memberships []*user_model.UserMembershipView) []*user_pb.Membership { +func MembershipsToMembershipsPb(memberships []*query.Membership) []*user_pb.Membership { converted := make([]*user_pb.Membership, len(memberships)) for i, membership := range memberships { converted[i] = MembershipToMembershipPb(membership) @@ -99,7 +100,7 @@ func MembershipsToMembershipsPb(memberships []*user_model.UserMembershipView) [] return converted } -func MembershipToMembershipPb(membership *user_model.UserMembershipView) *user_pb.Membership { +func MembershipToMembershipPb(membership *query.Membership) *user_pb.Membership { return &user_pb.Membership{ UserId: membership.UserID, Type: memberTypeToPb(membership), @@ -114,25 +115,23 @@ func MembershipToMembershipPb(membership *user_model.UserMembershipView) *user_p } } -func memberTypeToPb(membership *user_model.UserMembershipView) user_pb.MembershipType { - switch membership.MemberType { - case user_model.MemberTypeOrganisation: +func memberTypeToPb(membership *query.Membership) user_pb.MembershipType { + if membership.Org != nil { return &user_pb.Membership_OrgId{ - OrgId: membership.AggregateID, + OrgId: membership.Org.OrgID, } - case user_model.MemberTypeProject: + } else if membership.Project != nil { return &user_pb.Membership_ProjectId{ - ProjectId: membership.AggregateID, + ProjectId: membership.Project.ProjectID, } - case user_model.MemberTypeProjectGrant: + } else if membership.ProjectGrant != nil { return &user_pb.Membership_ProjectGrantId{ - ProjectGrantId: membership.ObjectID, + ProjectGrantId: membership.ProjectGrant.GrantID, } - case user_model.MemberTypeIam: + } else if membership.IAM != nil { return &user_pb.Membership_Iam{ - Iam: true, //TODO: ? + Iam: true, } - default: - return nil //TODO: ? } + return nil } diff --git a/internal/query/current_sequence.go b/internal/query/current_sequence.go index f2247b949a..130b7a4f28 100644 --- a/internal/query/current_sequence.go +++ b/internal/query/current_sequence.go @@ -38,11 +38,16 @@ func prepareLatestSequence() (sq.SelectBuilder, func(*sql.Row) (*LatestSequence, } } -func (q *Queries) latestSequence(ctx context.Context, projection table) (*LatestSequence, error) { +func (q *Queries) latestSequence(ctx context.Context, projections ...table) (*LatestSequence, error) { query, scan := prepareLatestSequence() - stmt, args, err := query.Where(sq.Eq{ - CurrentSequenceColProjectionName.identifier(): projection.name, - }).ToSql() + or := make(sq.Or, len(projections)) + for i, projection := range projections { + or[i] = sq.Eq{CurrentSequenceColProjectionName.identifier(): projection.name} + } + stmt, args, err := query. + Where(or). + OrderBy(CurrentSequenceColCurrentSequence.identifier()). + ToSql() if err != nil { return nil, errors.ThrowInternal(err, "QUERY-5CfX9", "Errors.Query.SQLStatement") } diff --git a/internal/query/iam_member.go b/internal/query/iam_member.go new file mode 100644 index 0000000000..e5332c102e --- /dev/null +++ b/internal/query/iam_member.go @@ -0,0 +1,38 @@ +package query + +import "github.com/caos/zitadel/internal/query/projection" + +var ( + iamMemberTable = table{ + name: projection.IAMMemberProjectionTable, + alias: "members", + } + IAMMemberUserID = Column{ + name: projection.MemberUserIDCol, + table: iamMemberTable, + } + IAMMemberRoles = Column{ + name: projection.MemberRolesCol, + table: iamMemberTable, + } + IAMMemberCreationDate = Column{ + name: projection.MemberCreationDate, + table: iamMemberTable, + } + IAMMemberChangeDate = Column{ + name: projection.MemberChangeDate, + table: iamMemberTable, + } + IAMMemberSequence = Column{ + name: projection.MemberSequence, + table: iamMemberTable, + } + IAMMemberResourceOwner = Column{ + name: projection.MemberResourceOwner, + table: iamMemberTable, + } + IAMMemberIAMID = Column{ + name: projection.IAMMemberIAMIDCol, + table: iamMemberTable, + } +) diff --git a/internal/query/member.go b/internal/query/member.go index b1ceca1552..8d67cef860 100644 --- a/internal/query/member.go +++ b/internal/query/member.go @@ -2,9 +2,67 @@ package query import ( "context" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/caos/zitadel/internal/query/projection" "github.com/caos/zitadel/internal/telemetry/tracing" ) +type MembersQuery struct { + SearchRequest + Queries []SearchQuery +} + +func (q *MembersQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func NewMemberEmailSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(HumanEmailCol, value, method) +} + +func NewMemberFirstNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(HumanFirstNameCol, value, method) +} + +func NewMemberLastNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(HumanLastNameCol, value, method) +} + +func NewMemberUserIDSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(memberUserID, value, TextEquals) +} +func NewMemberResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(memberResourceOwner, value, TextEquals) +} + +type Members struct { + SearchResponse + Members []*Member +} + +type Member struct { + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 + ResourceOwner string + + UserID string + Roles []string + PreferredLoginName string + Email string + FirstName string + LastName string + DisplayName string + AvatarURL string +} + func (r *Queries) IAMMemberByID(ctx context.Context, iamID, userID string) (member *IAMMemberReadModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -17,3 +75,18 @@ func (r *Queries) IAMMemberByID(ctx context.Context, iamID, userID string) (memb return member, nil } + +var ( + memberTableAlias = table{ + name: "members", + alias: "members", + } + memberUserID = Column{ + name: projection.MemberUserIDCol, + table: memberTableAlias, + } + memberResourceOwner = Column{ + name: projection.MemberResourceOwner, + table: memberTableAlias, + } +) diff --git a/internal/query/org_member.go b/internal/query/org_member.go new file mode 100644 index 0000000000..326ccf05ac --- /dev/null +++ b/internal/query/org_member.go @@ -0,0 +1,38 @@ +package query + +import "github.com/caos/zitadel/internal/query/projection" + +var ( + orgMemberTable = table{ + name: projection.OrgMemberProjectionTable, + alias: "members", + } + OrgMemberUserID = Column{ + name: projection.MemberUserIDCol, + table: orgMemberTable, + } + OrgMemberRoles = Column{ + name: projection.MemberRolesCol, + table: orgMemberTable, + } + OrgMemberCreationDate = Column{ + name: projection.MemberCreationDate, + table: orgMemberTable, + } + OrgMemberChangeDate = Column{ + name: projection.MemberChangeDate, + table: orgMemberTable, + } + OrgMemberSequence = Column{ + name: projection.MemberSequence, + table: orgMemberTable, + } + OrgMemberResourceOwner = Column{ + name: projection.MemberResourceOwner, + table: orgMemberTable, + } + OrgMemberOrgID = Column{ + name: projection.OrgMemberOrgIDCol, + table: orgMemberTable, + } +) diff --git a/internal/query/project_grant_member.go b/internal/query/project_grant_member.go new file mode 100644 index 0000000000..4e213264f4 --- /dev/null +++ b/internal/query/project_grant_member.go @@ -0,0 +1,42 @@ +package query + +import "github.com/caos/zitadel/internal/query/projection" + +var ( + projectGrantMemberTable = table{ + name: projection.ProjectGrantMemberProjectionTable, + alias: "members", + } + ProjectGrantMemberUserID = Column{ + name: projection.MemberUserIDCol, + table: projectGrantMemberTable, + } + ProjectGrantMemberRoles = Column{ + name: projection.MemberRolesCol, + table: projectGrantMemberTable, + } + ProjectGrantMemberCreationDate = Column{ + name: projection.MemberCreationDate, + table: projectGrantMemberTable, + } + ProjectGrantMemberChangeDate = Column{ + name: projection.MemberChangeDate, + table: projectGrantMemberTable, + } + ProjectGrantMemberSequence = Column{ + name: projection.MemberSequence, + table: projectGrantMemberTable, + } + ProjectGrantMemberResourceOwner = Column{ + name: projection.MemberResourceOwner, + table: projectGrantMemberTable, + } + ProjectGrantMemberProjectID = Column{ + name: projection.ProjectGrantMemberProjectIDCol, + table: projectGrantMemberTable, + } + ProjectGrantMemberGrantID = Column{ + name: projection.ProjectGrantMemberGrantIDCol, + table: projectGrantMemberTable, + } +) diff --git a/internal/query/project_member.go b/internal/query/project_member.go new file mode 100644 index 0000000000..97a1ead978 --- /dev/null +++ b/internal/query/project_member.go @@ -0,0 +1,38 @@ +package query + +import "github.com/caos/zitadel/internal/query/projection" + +var ( + projectMemberTable = table{ + name: projection.ProjectMemberProjectionTable, + alias: "members", + } + ProjectMemberUserID = Column{ + name: projection.MemberUserIDCol, + table: projectMemberTable, + } + ProjectMemberRoles = Column{ + name: projection.MemberRolesCol, + table: projectMemberTable, + } + ProjectMemberCreationDate = Column{ + name: projection.MemberCreationDate, + table: projectMemberTable, + } + ProjectMemberChangeDate = Column{ + name: projection.MemberChangeDate, + table: projectMemberTable, + } + ProjectMemberSequence = Column{ + name: projection.MemberSequence, + table: projectMemberTable, + } + ProjectMemberResourceOwner = Column{ + name: projection.MemberResourceOwner, + table: projectMemberTable, + } + ProjectMemberProjectID = Column{ + name: projection.ProjectMemberProjectIDCol, + table: projectMemberTable, + } +) diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index dd8cde6d7b..2da247b94b 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -47,7 +47,7 @@ const ( HumanUserIDCol = "user_id" // profile - HumanFistNameCol = "first_name" + HumanFirstNameCol = "first_name" HumanLastNameCol = "last_name" HumanNickNameCol = "nick_name" HumanDisplayNameCol = "display_name" @@ -208,7 +208,7 @@ func (p *UserProjection) reduceHumanAdded(event eventstore.EventReader) (*handle crdb.AddCreateStatement( []handler.Column{ handler.NewCol(HumanUserIDCol, e.Aggregate().ID), - handler.NewCol(HumanFistNameCol, e.FirstName), + handler.NewCol(HumanFirstNameCol, e.FirstName), handler.NewCol(HumanLastNameCol, e.LastName), handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}), handler.NewCol(HumanDisplayNameCol, &sql.NullString{String: e.DisplayName, Valid: e.DisplayName != ""}), @@ -244,7 +244,7 @@ func (p *UserProjection) reduceHumanRegistered(event eventstore.EventReader) (*h crdb.AddCreateStatement( []handler.Column{ handler.NewCol(HumanUserIDCol, e.Aggregate().ID), - handler.NewCol(HumanFistNameCol, e.FirstName), + handler.NewCol(HumanFirstNameCol, e.FirstName), handler.NewCol(HumanLastNameCol, e.LastName), handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}), handler.NewCol(HumanDisplayNameCol, &sql.NullString{String: e.DisplayName, Valid: e.DisplayName != ""}), @@ -381,7 +381,7 @@ func (p *UserProjection) reduceHumanProfileChanged(event eventstore.EventReader) } cols := make([]handler.Column, 0, 6) if e.FirstName != "" { - cols = append(cols, handler.NewCol(HumanFistNameCol, e.FirstName)) + cols = append(cols, handler.NewCol(HumanFirstNameCol, e.FirstName)) } if e.LastName != "" { diff --git a/internal/query/search_query.go b/internal/query/search_query.go index bae4cb4b70..4c634aaa12 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -46,6 +46,23 @@ type SearchQuery interface { toQuery(sq.SelectBuilder) sq.SelectBuilder } +type NotNullQuery struct { + Column Column +} + +func NewNotNullQuery(col Column) (*NotNullQuery, error) { + if col.isZero() { + return nil, ErrMissingColumn + } + return &NotNullQuery{ + Column: col, + }, nil +} + +func (q *NotNullQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + return query.Where(sq.NotEq{q.Column.identifier(): nil}) +} + type TextQuery struct { Column Column Text string diff --git a/internal/query/user.go b/internal/query/user.go index ae8cd17d65..6a14955502 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -2,7 +2,120 @@ package query import ( "context" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/query/projection" +) + +var ( + userTable = table{ + name: projection.UserTable, + } + UserIDCol = Column{ + name: projection.UserIDCol, + table: userTable, + } + UserCreationDateCol = Column{ + name: projection.UserCreationDateCol, + table: userTable, + } + UserChangeDateCol = Column{ + name: projection.UserChangeDateCol, + table: userTable, + } + UserResourceOwnerCol = Column{ + name: projection.UserResourceOwnerCol, + table: userTable, + } + UserStateCol = Column{ + name: projection.UserStateCol, + table: userTable, + } + UserSequenceCol = Column{ + name: projection.UserSequenceCol, + table: userTable, + } + UserUsernameCol = Column{ + name: projection.UserUsernameCol, + table: userTable, + } +) + +var ( + humanTable = table{ + name: projection.UserHumanTable, + } + // profile + HumanUserIDCol = Column{ + name: projection.HumanUserIDCol, + table: humanTable, + } + HumanFirstNameCol = Column{ + name: projection.HumanFirstNameCol, + table: humanTable, + } + HumanLastNameCol = Column{ + name: projection.HumanLastNameCol, + table: humanTable, + } + HumanNickNameCol = Column{ + name: projection.HumanNickNameCol, + table: humanTable, + } + HumanDisplayNameCol = Column{ + name: projection.HumanDisplayNameCol, + table: humanTable, + } + HumanPreferredLanguageCol = Column{ + name: projection.HumanPreferredLanguageCol, + table: humanTable, + } + HumanGenderCol = Column{ + name: projection.HumanGenderCol, + table: humanTable, + } + HumanAvaterURLCol = Column{ + name: projection.HumanAvaterURLCol, + table: humanTable, + } + + // email + HumanEmailCol = Column{ + name: projection.HumanEmailCol, + table: humanTable, + } + HumanIsEmailVerifiedCol = Column{ + name: projection.HumanIsEmailVerifiedCol, + table: humanTable, + } + + // phone + HumanPhoneCol = Column{ + name: projection.HumanPhoneCol, + table: humanTable, + } + HumanIsPhoneVerifiedCol = Column{ + name: projection.HumanIsPhoneVerifiedCol, + table: humanTable, + } +) + +var ( + machineTable = table{ + name: projection.UserMachineTable, + } + MachineUserIDCol = Column{ + name: projection.MachineUserIDCol, + table: machineTable, + } + MachineNameCol = Column{ + name: projection.MachineNameCol, + table: machineTable, + } + MachineDescriptionCol = Column{ + name: projection.MachineDescriptionCol, + table: machineTable, + } ) func (q *Queries) UserEvents(ctx context.Context, orgID, userID string, sequence uint64) ([]eventstore.EventReader, error) { diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go new file mode 100644 index 0000000000..5b65f92cd2 --- /dev/null +++ b/internal/query/user_membership.go @@ -0,0 +1,330 @@ +package query + +import ( + "context" + "database/sql" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query/projection" + "github.com/lib/pq" +) + +type Memberships struct { + SearchResponse + Memberships []*Membership +} + +type Membership struct { + UserID string + Roles []string + CreationDate time.Time + ChangeDate time.Time + Sequence uint64 + ResourceOwner string + DisplayName string + + Org *OrgMembership + IAM *IAMMembership + Project *ProjectMembership + ProjectGrant *ProjectGrantMembership +} + +type OrgMembership struct { + OrgID string +} + +type IAMMembership struct { + IAMID string +} + +type ProjectMembership struct { + ProjectID string +} + +type ProjectGrantMembership struct { + ProjectID string + GrantID string +} + +type MembershipSearchQuery struct { + SearchRequest + Queries []SearchQuery +} + +func NewMembershipUserIDQuery(userID string) (SearchQuery, error) { + return NewTextQuery(membershipUserID.setTable(membershipAlias), userID, TextEquals) +} + +func NewMembershipResourceOwnerQuery(value string) (SearchQuery, error) { + return NewTextQuery(membershipResourceOwner.setTable(membershipAlias), value, TextEquals) +} + +func NewMembershipOrgIDQuery(value string) (SearchQuery, error) { + return NewTextQuery(membershipOrgID, value, TextEquals) +} + +func NewMembershipProjectIDQuery(value string) (SearchQuery, error) { + return NewTextQuery(membershipProjectID, value, TextEquals) +} + +func NewMembershipProjectGrantIDQuery(value string) (SearchQuery, error) { + return NewTextQuery(membershipGrantID, value, TextEquals) +} + +func NewMembershipIsIAMQuery() (SearchQuery, error) { + return NewNotNullQuery(membershipIAMID) +} + +func (q *MembershipSearchQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuery) (*Memberships, error) { + query, scan := prepareMembershipsQuery() + stmt, args, err := queries.toQuery(query).ToSql() + if err != nil { + return nil, errors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") + } + latestSequence, err := q.latestSequence(ctx, orgMemberTable, iamMemberTable, projectMemberTable, projectGrantMemberTable) + if err != nil { + return nil, err + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-eAV2x", "Errors.Internal") + } + memberships, err := scan(rows) + if err != nil { + return nil, err + } + memberships.LatestSequence = latestSequence + return memberships, nil +} + +var ( + //membershipAlias is a hack to satisfy checks in the queries + membershipAlias = table{ + name: "memberships", + } + membershipUserID = Column{ + name: projection.MemberUserIDCol, + table: membershipAlias, + } + membershipRoles = Column{ + name: projection.MemberRolesCol, + table: membershipAlias, + } + membershipCreationDate = Column{ + name: projection.MemberCreationDate, + table: membershipAlias, + } + membershipChangeDate = Column{ + name: projection.MemberChangeDate, + table: membershipAlias, + } + membershipSequence = Column{ + name: projection.MemberSequence, + table: membershipAlias, + } + membershipResourceOwner = Column{ + name: projection.MemberResourceOwner, + table: membershipAlias, + } + membershipOrgID = Column{ + name: projection.OrgMemberOrgIDCol, + table: membershipAlias, + } + membershipIAMID = Column{ + name: projection.IAMMemberIAMIDCol, + table: membershipAlias, + } + membershipProjectID = Column{ + name: projection.ProjectMemberProjectIDCol, + table: membershipAlias, + } + membershipGrantID = Column{ + name: projection.ProjectGrantMemberGrantIDCol, + table: membershipAlias, + } + + membershipFrom = "(" + + prepareOrgMember() + + " UNION ALL " + + prepareIAMMember() + + " UNION ALL " + + prepareProjectMember() + + " UNION ALL " + + prepareProjectGrantMember() + + ") AS " + membershipAlias.identifier() +) + +func prepareMembershipsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { + return sq.Select( + membershipUserID.identifier(), + membershipRoles.identifier(), + membershipCreationDate.identifier(), + membershipChangeDate.identifier(), + membershipSequence.identifier(), + membershipResourceOwner.identifier(), + membershipOrgID.identifier(), + membershipIAMID.identifier(), + membershipProjectID.identifier(), + membershipGrantID.identifier(), + HumanDisplayNameCol.identifier(), + MachineNameCol.identifier(), + countColumn.identifier(), + ).From(membershipFrom). + LeftJoin(join(HumanUserIDCol, membershipUserID)). + LeftJoin(join(MachineUserIDCol, membershipUserID)). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*Memberships, error) { + memberships := make([]*Membership, 0) + var count uint64 + for rows.Next() { + + var ( + membership = new(Membership) + orgID = sql.NullString{} + iamID = sql.NullString{} + projectID = sql.NullString{} + grantID = sql.NullString{} + roles = pq.StringArray{} + displayName = sql.NullString{} + machineName = sql.NullString{} + ) + + err := rows.Scan( + &membership.UserID, + &roles, + &membership.CreationDate, + &membership.ChangeDate, + &membership.Sequence, + &membership.ResourceOwner, + &orgID, + &iamID, + &projectID, + &grantID, + &displayName, + &machineName, + &count, + ) + + if err != nil { + return nil, err + } + + membership.Roles = roles + + if displayName.Valid { + membership.DisplayName = displayName.String + } else if machineName.Valid { + membership.DisplayName = machineName.String + } + + if orgID.Valid { + membership.Org = &OrgMembership{ + OrgID: orgID.String, + } + } else if iamID.Valid { + membership.IAM = &IAMMembership{ + IAMID: iamID.String, + } + } else if projectID.Valid && grantID.Valid { + membership.ProjectGrant = &ProjectGrantMembership{ + ProjectID: projectID.String, + GrantID: grantID.String, + } + } else if projectID.Valid { + membership.Project = &ProjectMembership{ + ProjectID: projectID.String, + } + } + + memberships = append(memberships, membership) + } + + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-N34NV", "Errors.Query.CloseRows") + } + + return &Memberships{ + Memberships: memberships, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +func prepareOrgMember() string { + stmt, _ := sq.Select( + OrgMemberUserID.identifier(), + OrgMemberRoles.identifier(), + OrgMemberCreationDate.identifier(), + OrgMemberChangeDate.identifier(), + OrgMemberSequence.identifier(), + OrgMemberResourceOwner.identifier(), + OrgMemberOrgID.identifier(), + "NULL::STRING AS "+membershipIAMID.name, + "NULL::STRING AS "+membershipProjectID.name, + "NULL::STRING AS "+membershipGrantID.name, + ).From(orgMemberTable.identifier()).MustSql() + return stmt +} + +func prepareIAMMember() string { + stmt, _ := sq.Select( + IAMMemberUserID.identifier(), + IAMMemberRoles.identifier(), + IAMMemberCreationDate.identifier(), + IAMMemberChangeDate.identifier(), + IAMMemberSequence.identifier(), + IAMMemberResourceOwner.identifier(), + "NULL::STRING AS "+membershipOrgID.name, + IAMMemberIAMID.identifier(), + "NULL::STRING AS "+membershipProjectID.name, + "NULL::STRING AS "+membershipGrantID.name, + ).From(iamMemberTable.identifier()).MustSql() + return stmt +} + +func prepareProjectMember() string { + stmt, _ := sq.Select( + ProjectMemberUserID.identifier(), + ProjectMemberRoles.identifier(), + ProjectMemberCreationDate.identifier(), + ProjectMemberChangeDate.identifier(), + ProjectMemberSequence.identifier(), + ProjectMemberResourceOwner.identifier(), + "NULL::STRING AS "+membershipOrgID.name, + "NULL::STRING AS "+membershipIAMID.name, + ProjectMemberProjectID.identifier(), + "NULL::STRING AS "+membershipGrantID.name, + ).From(projectMemberTable.identifier()).MustSql() + + return stmt +} + +func prepareProjectGrantMember() string { + stmt, _ := sq.Select( + ProjectGrantMemberUserID.identifier(), + ProjectGrantMemberRoles.identifier(), + ProjectGrantMemberCreationDate.identifier(), + ProjectGrantMemberChangeDate.identifier(), + ProjectGrantMemberSequence.identifier(), + ProjectGrantMemberResourceOwner.identifier(), + "NULL::STRING AS "+membershipOrgID.name, + "NULL::STRING AS "+membershipIAMID.name, + ProjectGrantMemberProjectID.identifier(), + ProjectGrantMemberGrantID.identifier(), + ).From(projectGrantMemberTable.identifier()).MustSql() + + return stmt +} diff --git a/internal/query/user_membership_test.go b/internal/query/user_membership_test.go new file mode 100644 index 0000000000..4a81050284 --- /dev/null +++ b/internal/query/user_membership_test.go @@ -0,0 +1,611 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/lib/pq" +) + +var ( + membershipsStmt = regexp.QuoteMeta( + "SELECT memberships.user_id" + + ", memberships.roles" + + ", memberships.creation_date" + + ", memberships.change_date" + + ", memberships.sequence" + + ", memberships.resource_owner" + + ", memberships.org_id" + + ", memberships.iam_id" + + ", memberships.project_id" + + ", memberships.grant_id" + + ", zitadel.projections.users_humans.display_name" + + ", zitadel.projections.users_machines.name" + + ", COUNT(*) OVER ()" + + " FROM (" + + "SELECT members.user_id" + + ", members.roles" + + ", members.creation_date" + + ", members.change_date" + + ", members.sequence" + + ", members.resource_owner" + + ", members.org_id" + + ", NULL::STRING AS iam_id" + + ", NULL::STRING AS project_id" + + ", NULL::STRING AS grant_id" + + " FROM zitadel.projections.org_members as members" + + " UNION ALL " + + "SELECT members.user_id" + + ", members.roles" + + ", members.creation_date" + + ", members.change_date" + + ", members.sequence" + + ", members.resource_owner" + + ", NULL::STRING AS org_id" + + ", members.iam_id" + + ", NULL::STRING AS project_id" + + ", NULL::STRING AS grant_id" + + " FROM zitadel.projections.iam_members as members" + + " UNION ALL " + + "SELECT members.user_id" + + ", members.roles" + + ", members.creation_date" + + ", members.change_date" + + ", members.sequence" + + ", members.resource_owner" + + ", NULL::STRING AS org_id" + + ", NULL::STRING AS iam_id" + + ", members.project_id" + + ", NULL::STRING AS grant_id" + + " FROM zitadel.projections.project_members as members" + + " UNION ALL " + + "SELECT members.user_id" + + ", members.roles" + + ", members.creation_date" + + ", members.change_date" + + ", members.sequence" + + ", members.resource_owner" + + ", NULL::STRING AS org_id" + + ", NULL::STRING AS iam_id" + + ", members.project_id" + + ", members.grant_id" + + " FROM zitadel.projections.project_grant_members as members" + + ") AS memberships" + + " LEFT JOIN zitadel.projections.users_humans ON memberships.user_id = zitadel.projections.users_humans.user_id" + + " LEFT JOIN zitadel.projections.users_machines ON memberships.user_id = zitadel.projections.users_machines.user_id") + membershipCols = []string{ + "user_id", + "roles", + "creation_date", + "change_date", + "sequence", + "resource_owner", + "org_id", + "iam_id", + "project_id", + "grant_id", + "display_name", + "name", + "count", + } +) + +func Test_MembershipPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareMembershipsQuery no result", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + nil, + nil, + ), + }, + object: &Memberships{Memberships: []*Membership{}}, + }, + { + name: "prepareMembershipsQuery one org member human", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + "org-id", + nil, + nil, + nil, + "display name", + nil, + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + Org: &OrgMembership{OrgID: "org-id"}, + DisplayName: "display name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one org member machine", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + "org-id", + nil, + nil, + nil, + nil, + "machine-name", + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + Org: &OrgMembership{OrgID: "org-id"}, + DisplayName: "machine-name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one iam member human", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + "iam-id", + nil, + nil, + "display name", + nil, + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + IAM: &IAMMembership{IAMID: "iam-id"}, + DisplayName: "display name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one iam member machine", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + "iam-id", + nil, + nil, + nil, + "machine-name", + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + IAM: &IAMMembership{IAMID: "iam-id"}, + DisplayName: "machine-name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one project member human", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + nil, + "project-id", + nil, + "display name", + nil, + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + Project: &ProjectMembership{ProjectID: "project-id"}, + DisplayName: "display name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one project member machine", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + nil, + "project-id", + nil, + nil, + "machine-name", + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + Project: &ProjectMembership{ProjectID: "project-id"}, + DisplayName: "machine-name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one project grant member human", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + nil, + "project-id", + "grant-id", + "display name", + nil, + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + ProjectGrant: &ProjectGrantMembership{ + GrantID: "grant-id", + ProjectID: "project-id", + }, + DisplayName: "display name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one project grant member machine", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + nil, + "project-id", + "grant-id", + nil, + "machine-name", + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + ProjectGrant: &ProjectGrantMembership{ + GrantID: "grant-id", + ProjectID: "project-id", + }, + DisplayName: "machine-name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery one for each member type", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueries( + membershipsStmt, + membershipCols, + [][]driver.Value{ + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + "org-id", + nil, + nil, + nil, + "display name", + nil, + }, + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + "iam-id", + nil, + nil, + "display name", + nil, + }, + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + nil, + "project-id", + nil, + "display name", + nil, + }, + { + "user-id", + pq.StringArray{"role1", "role2"}, + testNow, + testNow, + uint64(20211202), + "ro", + nil, + nil, + "project-id", + "grant-id", + "display name", + nil, + }, + }, + ), + }, + object: &Memberships{ + SearchResponse: SearchResponse{ + Count: 4, + }, + Memberships: []*Membership{ + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + Org: &OrgMembership{OrgID: "org-id"}, + DisplayName: "display name", + }, + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + IAM: &IAMMembership{IAMID: "iam-id"}, + DisplayName: "display name", + }, + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + Project: &ProjectMembership{ProjectID: "project-id"}, + DisplayName: "display name", + }, + { + UserID: "user-id", + Roles: []string{"role1", "role2"}, + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211202, + ResourceOwner: "ro", + ProjectGrant: &ProjectGrantMembership{ + ProjectID: "project-id", + GrantID: "grant-id", + }, + DisplayName: "display name", + }, + }, + }, + }, + { + name: "prepareMembershipsQuery sql err", + prepare: prepareMembershipsQuery, + want: want{ + sqlExpectations: mockQueryErr( + membershipsStmt, + 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) + }) + } +}