feat(queries): user membership (#2768)

* refactor(domain): add user type

* fix(projections): start with login names

* fix(login_policy): correct handling of user domain claimed event

* fix(projections): add members

* refactor: simplify member projections

* add migration for members

* add metadata to member projections

* refactor: login name projection

* fix: set correct suffixes on login name projections

* test(projections): login name reduces

* fix: correct cols in reduce member

* test(projections): org, iam, project members

* member additional cols and conds as opt,
add project grant members

* fix(migration): members

* fix(migration): correct database name

* migration version

* migs

* better naming for member cond and col

* split project and project grant members

* prepare member columns

* feat(queries): membership query

* test(queries): membership prepare

* fix(queries): multiple projections for latest sequence

* fix(api): use query for membership queries in auth and management

* fix(query): member queries and user avatar column

* member cols

* fix(queries): membership stmt

* fix user test

* fix user test
This commit is contained in:
Silvan 2021-12-14 08:19:02 +01:00 committed by GitHub
parent 2cdb297138
commit 2265fffd8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1385 additions and 66 deletions

View File

@ -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) { 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 { if err != nil {
return nil, err return nil, err
} }
response, err := s.repo.SearchMyUserMemberships(ctx, request) response, err := s.query.Memberships(ctx, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &auth_pb.ListMyMembershipsResponse{ return &auth_pb.ListMyMembershipsResponse{
Result: user_grpc.MembershipsToMembershipsPb(response.Result), Result: user_grpc.MembershipsToMembershipsPb(response.Memberships),
Details: obj_grpc.ToListDetails( Details: obj_grpc.ToListDetails(
response.TotalResult, response.Count,
response.Sequence, response.Sequence,
response.Timestamp, response.Timestamp,
), ),

View File

@ -1,23 +1,33 @@
package auth package auth
import ( import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/api/grpc/object"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user" 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" 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) offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := user_grpc.MembershipQueriesToModel(req.Queries) queries, err := user_grpc.MembershipQueriesToQuery(req.Queries)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &user_model.UserMembershipSearchRequest{ 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, Offset: offset,
Limit: limit, Limit: limit,
Asc: asc, Asc: asc,
//SortingColumn: //TODO: sorting //SortingColumn: //TODO: sorting
},
Queries: queries, Queries: queries,
}, nil }, nil
} }

View File

@ -7,7 +7,6 @@ import (
"github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/change" "github.com/caos/zitadel/internal/api/grpc/change"
"github.com/caos/zitadel/internal/api/grpc/metadata" "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" obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/api/grpc/org" "github.com/caos/zitadel/internal/api/grpc/org"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user" 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 nil, err
} }
return &auth_pb.UpdateMyUserNameResponse{ return &auth_pb.UpdateMyUserNameResponse{
Details: object.DomainToChangeDetailsPb(objectDetails), Details: obj_grpc.DomainToChangeDetailsPb(objectDetails),
}, nil }, nil
} }
@ -121,7 +120,7 @@ func (s *Server) ListMyUserGrants(ctx context.Context, req *auth_pb.ListMyUserGr
} }
return &auth_pb.ListMyUserGrantsResponse{ return &auth_pb.ListMyUserGrantsResponse{
Result: UserGrantsToPb(res.Result), Result: UserGrantsToPb(res.Result),
Details: object.ToListDetails( Details: obj_grpc.ToListDetails(
res.TotalResult, res.TotalResult,
res.Sequence, res.Sequence,
res.Timestamp, res.Timestamp,
@ -140,13 +139,13 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje
} }
return &auth_pb.ListMyProjectOrgsResponse{ return &auth_pb.ListMyProjectOrgsResponse{
//TODO: not all details //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), Result: org.OrgsToPb(res.Result),
}, nil }, nil
} }
func ListMyProjectOrgsRequestToModel(req *auth_pb.ListMyProjectOrgsRequest) (*grant_model.UserGrantSearchRequest, error) { 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) queries, err := org.OrgQueriesToUserGrantModel(req.Queries)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -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) { 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 { if err != nil {
return nil, err return nil, err
} }
response, err := s.user.SearchUserMemberships(ctx, request) response, err := s.query.Memberships(ctx, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mgmt_pb.ListUserMembershipsResponse{ return &mgmt_pb.ListUserMembershipsResponse{
Result: user_grpc.MembershipsToMembershipsPb(response.Result), Result: user_grpc.MembershipsToMembershipsPb(response.Memberships),
Details: obj_grpc.ToListDetails( Details: obj_grpc.ToListDetails(
response.TotalResult, response.Count,
response.Sequence, response.Sequence,
response.Timestamp, response.Timestamp,
), ),

View File

@ -255,21 +255,27 @@ func ListHumanLinkedIDPsRequestToQuery(ctx context.Context, req *mgmt_pb.ListHum
}, nil }, 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) offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := user_grpc.MembershipQueriesToModel(req.Queries) queries, err := user_grpc.MembershipQueriesToQuery(req.Queries)
if err != nil { if err != nil {
return nil, err return nil, err
} }
queries = append(queries, &user_model.UserMembershipSearchQuery{ userQuery, err := query.NewMembershipUserIDQuery(req.UserId)
Key: user_model.UserMembershipSearchKeyUserID, if err != nil {
Method: domain.SearchMethodEquals, return nil, err
Value: req.UserId, }
}) ownerQuery, err := query.NewMembershipResourceOwnerQuery(authz.GetCtxData(ctx).OrgID)
return &user_model.UserMembershipSearchRequest{ if err != nil {
return nil, err
}
queries = append(queries, userQuery, ownerQuery)
return &query.MembershipSearchQuery{
SearchRequest: query.SearchRequest{
Offset: offset, Offset: offset,
Limit: limit, Limit: limit,
Asc: asc, Asc: asc,
},
//SortingColumn: //TODO: sorting //SortingColumn: //TODO: sorting
Queries: queries, Queries: queries,
}, nil }, nil

View File

@ -124,7 +124,7 @@ func TextMethodToQuery(method object_pb.TextQueryMethod) query.TextComparison {
func ListQueryToModel(query *object_pb.ListQuery) (offset, limit uint64, asc bool) { func ListQueryToModel(query *object_pb.ListQuery) (offset, limit uint64, asc bool) {
if query == nil { if query == nil {
return return 0, 0, false
} }
return query.Offset, uint64(query.Limit), query.Asc return query.Offset, uint64(query.Limit), query.Asc
} }

View File

@ -4,34 +4,35 @@ import (
"github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/query"
user_model "github.com/caos/zitadel/internal/user/model" user_model "github.com/caos/zitadel/internal/user/model"
user_pb "github.com/caos/zitadel/pkg/grpc/user" user_pb "github.com/caos/zitadel/pkg/grpc/user"
) )
func MembershipQueriesToModel(queries []*user_pb.MembershipQuery) (_ []*user_model.UserMembershipSearchQuery, err error) { func MembershipQueriesToQuery(queries []*user_pb.MembershipQuery) (_ []query.SearchQuery, err error) {
q := make([]*user_model.UserMembershipSearchQuery, 0) q := make([]query.SearchQuery, 0)
for _, query := range queries { for _, query := range queries {
qs, err := MembershipQueryToModel(query) qs, err := MembershipQueryToQuery(query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
q = append(q, qs...) q = append(q, qs)
} }
return q, nil return q, nil
} }
func MembershipQueryToModel(query *user_pb.MembershipQuery) ([]*user_model.UserMembershipSearchQuery, error) { func MembershipQueryToQuery(req *user_pb.MembershipQuery) (query.SearchQuery, error) {
switch q := query.Query.(type) { switch q := req.Query.(type) {
case *user_pb.MembershipQuery_OrgQuery: case *user_pb.MembershipQuery_OrgQuery:
return MembershipOrgQueryToModel(q.OrgQuery), nil return query.NewMembershipOrgIDQuery(q.OrgQuery.OrgId)
case *user_pb.MembershipQuery_ProjectQuery: case *user_pb.MembershipQuery_ProjectQuery:
return MembershipProjectQueryToModel(q.ProjectQuery), nil return query.NewMembershipProjectIDQuery(q.ProjectQuery.ProjectId)
case *user_pb.MembershipQuery_ProjectGrantQuery: case *user_pb.MembershipQuery_ProjectGrantQuery:
return MembershipProjectGrantQueryToModel(q.ProjectGrantQuery), nil return query.NewMembershipProjectGrantIDQuery(q.ProjectGrantQuery.ProjectGrantId)
case *user_pb.MembershipQuery_IamQuery: case *user_pb.MembershipQuery_IamQuery:
return MembershipIAMQueryToModel(q.IamQuery), nil return query.NewMembershipIsIAMQuery()
default: 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)) converted := make([]*user_pb.Membership, len(memberships))
for i, membership := range memberships { for i, membership := range memberships {
converted[i] = MembershipToMembershipPb(membership) converted[i] = MembershipToMembershipPb(membership)
@ -99,7 +100,7 @@ func MembershipsToMembershipsPb(memberships []*user_model.UserMembershipView) []
return converted return converted
} }
func MembershipToMembershipPb(membership *user_model.UserMembershipView) *user_pb.Membership { func MembershipToMembershipPb(membership *query.Membership) *user_pb.Membership {
return &user_pb.Membership{ return &user_pb.Membership{
UserId: membership.UserID, UserId: membership.UserID,
Type: memberTypeToPb(membership), Type: memberTypeToPb(membership),
@ -114,25 +115,23 @@ func MembershipToMembershipPb(membership *user_model.UserMembershipView) *user_p
} }
} }
func memberTypeToPb(membership *user_model.UserMembershipView) user_pb.MembershipType { func memberTypeToPb(membership *query.Membership) user_pb.MembershipType {
switch membership.MemberType { if membership.Org != nil {
case user_model.MemberTypeOrganisation:
return &user_pb.Membership_OrgId{ 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{ 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{ 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{ return &user_pb.Membership_Iam{
Iam: true, //TODO: ? Iam: true,
} }
default:
return nil //TODO: ?
} }
return nil
} }

View File

@ -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() query, scan := prepareLatestSequence()
stmt, args, err := query.Where(sq.Eq{ or := make(sq.Or, len(projections))
CurrentSequenceColProjectionName.identifier(): projection.name, for i, projection := range projections {
}).ToSql() or[i] = sq.Eq{CurrentSequenceColProjectionName.identifier(): projection.name}
}
stmt, args, err := query.
Where(or).
OrderBy(CurrentSequenceColCurrentSequence.identifier()).
ToSql()
if err != nil { if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-5CfX9", "Errors.Query.SQLStatement") return nil, errors.ThrowInternal(err, "QUERY-5CfX9", "Errors.Query.SQLStatement")
} }

View File

@ -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,
}
)

View File

@ -2,9 +2,67 @@ package query
import ( import (
"context" "context"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/query/projection"
"github.com/caos/zitadel/internal/telemetry/tracing" "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) { func (r *Queries) IAMMemberByID(ctx context.Context, iamID, userID string) (member *IAMMemberReadModel, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@ -17,3 +75,18 @@ func (r *Queries) IAMMemberByID(ctx context.Context, iamID, userID string) (memb
return member, nil return member, nil
} }
var (
memberTableAlias = table{
name: "members",
alias: "members",
}
memberUserID = Column{
name: projection.MemberUserIDCol,
table: memberTableAlias,
}
memberResourceOwner = Column{
name: projection.MemberResourceOwner,
table: memberTableAlias,
}
)

View File

@ -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,
}
)

View File

@ -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,
}
)

View File

@ -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,
}
)

View File

@ -47,7 +47,7 @@ const (
HumanUserIDCol = "user_id" HumanUserIDCol = "user_id"
// profile // profile
HumanFistNameCol = "first_name" HumanFirstNameCol = "first_name"
HumanLastNameCol = "last_name" HumanLastNameCol = "last_name"
HumanNickNameCol = "nick_name" HumanNickNameCol = "nick_name"
HumanDisplayNameCol = "display_name" HumanDisplayNameCol = "display_name"
@ -208,7 +208,7 @@ func (p *UserProjection) reduceHumanAdded(event eventstore.EventReader) (*handle
crdb.AddCreateStatement( crdb.AddCreateStatement(
[]handler.Column{ []handler.Column{
handler.NewCol(HumanUserIDCol, e.Aggregate().ID), handler.NewCol(HumanUserIDCol, e.Aggregate().ID),
handler.NewCol(HumanFistNameCol, e.FirstName), handler.NewCol(HumanFirstNameCol, e.FirstName),
handler.NewCol(HumanLastNameCol, e.LastName), handler.NewCol(HumanLastNameCol, e.LastName),
handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}), handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}),
handler.NewCol(HumanDisplayNameCol, &sql.NullString{String: e.DisplayName, Valid: e.DisplayName != ""}), 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( crdb.AddCreateStatement(
[]handler.Column{ []handler.Column{
handler.NewCol(HumanUserIDCol, e.Aggregate().ID), handler.NewCol(HumanUserIDCol, e.Aggregate().ID),
handler.NewCol(HumanFistNameCol, e.FirstName), handler.NewCol(HumanFirstNameCol, e.FirstName),
handler.NewCol(HumanLastNameCol, e.LastName), handler.NewCol(HumanLastNameCol, e.LastName),
handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}), handler.NewCol(HumanNickNameCol, &sql.NullString{String: e.NickName, Valid: e.NickName != ""}),
handler.NewCol(HumanDisplayNameCol, &sql.NullString{String: e.DisplayName, Valid: e.DisplayName != ""}), 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) cols := make([]handler.Column, 0, 6)
if e.FirstName != "" { if e.FirstName != "" {
cols = append(cols, handler.NewCol(HumanFistNameCol, e.FirstName)) cols = append(cols, handler.NewCol(HumanFirstNameCol, e.FirstName))
} }
if e.LastName != "" { if e.LastName != "" {

View File

@ -46,6 +46,23 @@ type SearchQuery interface {
toQuery(sq.SelectBuilder) sq.SelectBuilder 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 { type TextQuery struct {
Column Column Column Column
Text string Text string

View File

@ -2,7 +2,120 @@ package query
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/eventstore" "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) { func (q *Queries) UserEvents(ctx context.Context, orgID, userID string, sequence uint64) ([]eventstore.EventReader, error) {

View File

@ -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
}

View File

@ -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)
})
}
}