feat: user memberships (#537)

* feat: add search user memberships

* feat: add search user memberships

* feat: read user member ship

* feat: add usergrant search key

* feat: uesrmemberships based on permissions

* feat: merge master

* fix: correct permissions

* fix: update display name on change profile

* fix: merge request converations

* fix: err handling

* Update internal/user/model/user_membership_view.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Fabi
2020-07-30 14:37:55 +02:00
committed by GitHub
parent 4dabecd8d4
commit 75f1c4c576
60 changed files with 3773 additions and 2174 deletions

View File

@@ -0,0 +1,8 @@
package eventstore
const (
projectReadPerm = "project.read"
orgMemberReadPerm = "org.member.read"
projectMemberReadPerm = "project.member.read"
projectGrantMemberReadPerm = "project.member.read"
)

View File

@@ -3,6 +3,8 @@ package eventstore
import (
"context"
caos_errs "github.com/caos/zitadel/internal/errors"
global_model "github.com/caos/zitadel/internal/model"
"github.com/caos/zitadel/internal/view/repository"
"github.com/caos/logging"
@@ -96,7 +98,7 @@ func (repo *UserRepo) SearchUsers(ctx context.Context, request *usr_model.UserSe
request.EnsureLimit(repo.SearchLimit)
sequence, err := repo.View.GetLatestUserSequence()
logging.Log("EVENT-Lcn7d").OnError(err).Warn("could not read latest user sequence")
projects, count, err := repo.View.SearchUsers(request)
users, count, err := repo.View.SearchUsers(request)
if err != nil {
return nil, err
}
@@ -104,7 +106,7 @@ func (repo *UserRepo) SearchUsers(ctx context.Context, request *usr_model.UserSe
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
Result: model.UsersToModel(projects),
Result: model.UsersToModel(users),
}
if err == nil {
result.Sequence = sequence.CurrentSequence
@@ -215,3 +217,71 @@ func (repo *UserRepo) AddressByID(ctx context.Context, userID string) (*usr_mode
func (repo *UserRepo) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) {
return repo.UserEvents.ChangeAddress(ctx, address)
}
func (repo *UserRepo) SearchUserMemberships(ctx context.Context, request *usr_model.UserMembershipSearchRequest) (*usr_model.UserMembershipSearchResponse, error) {
request.EnsureLimit(repo.SearchLimit)
sequence, err := repo.View.GetLatestUserMembershipSequence()
logging.Log("EVENT-Dn7sf").OnError(err).Warn("could not read latest user sequence")
result := handleSearchUserMembershipsPermissions(ctx, request, sequence)
if result != nil {
return result, nil
}
memberships, count, err := repo.View.SearchUserMemberships(request)
if err != nil {
return nil, err
}
result = &usr_model.UserMembershipSearchResponse{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
Result: model.UserMembershipsToModel(memberships),
}
if err == nil {
result.Sequence = sequence.CurrentSequence
result.Timestamp = sequence.CurrentTimestamp
}
return result, nil
}
func handleSearchUserMembershipsPermissions(ctx context.Context, request *usr_model.UserMembershipSearchRequest, sequence *repository.CurrentSequence) *usr_model.UserMembershipSearchResponse {
permissions := authz.GetAllPermissionsFromCtx(ctx)
orgPerm := authz.HasGlobalExplicitPermission(permissions, orgMemberReadPerm)
projectPerm := authz.HasGlobalExplicitPermission(permissions, projectMemberReadPerm)
projectGrantPerm := authz.HasGlobalExplicitPermission(permissions, projectGrantMemberReadPerm)
if orgPerm && projectPerm && projectGrantPerm {
return nil
}
if !orgPerm {
request.Queries = append(request.Queries, &usr_model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyMemberType, Method: global_model.SearchMethodNotEquals, Value: usr_model.MemberTypeOrganisation})
}
ids := authz.GetExplicitPermissionCtxIDs(permissions, projectMemberReadPerm)
ids = append(ids, authz.GetExplicitPermissionCtxIDs(permissions, projectGrantMemberReadPerm)...)
if _, q := request.GetSearchQuery(usr_model.UserMembershipSearchKeyObjectID); q != nil {
containsID := false
for _, id := range ids {
if id == q.Value {
containsID = true
break
}
}
if !containsID {
result := &usr_model.UserMembershipSearchResponse{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(0),
Result: []*usr_model.UserMembershipView{},
}
if sequence != nil {
result.Sequence = sequence.CurrentSequence
result.Timestamp = sequence.CurrentTimestamp
}
return result
}
}
request.Queries = append(request.Queries, &usr_model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyObjectID, Method: global_model.SearchMethodIsOneOf, Value: ids})
return nil
}

View File

@@ -13,10 +13,6 @@ import (
"github.com/caos/zitadel/internal/view/repository"
)
const (
projectReadPerm = "project.read"
)
type UserGrantRepo struct {
SearchLimit uint64
UserGrantEvents *grant_event.UserGrantEventStore

View File

@@ -44,6 +44,7 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev
&Org{handler: handler{view, bulkLimit, configs.cycleDuration("Org"), errorCount}},
&OrgMember{handler: handler{view, bulkLimit, configs.cycleDuration("OrgMember"), errorCount}, userEvents: repos.UserEvents},
&OrgDomain{handler: handler{view, bulkLimit, configs.cycleDuration("OrgDomain"), errorCount}},
&UserMembership{handler: handler{view, bulkLimit, configs.cycleDuration("UserMembership"), errorCount}, orgEvents: repos.OrgEvents, projectEvents: repos.ProjectEvents},
}
}

View File

@@ -0,0 +1,171 @@
package handler
import (
"context"
org_model "github.com/caos/zitadel/internal/org/model"
org_event "github.com/caos/zitadel/internal/org/repository/eventsourcing"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
usr_model "github.com/caos/zitadel/internal/user/model"
usr_es_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
type UserMembership struct {
handler
orgEvents *org_event.OrgEventstore
projectEvents *proj_event.ProjectEventstore
}
const (
userMembershipTable = "management.user_memberships"
)
func (m *UserMembership) ViewModel() string {
return userMembershipTable
}
func (m *UserMembership) EventQuery() (*models.SearchQuery, error) {
sequence, err := m.view.GetLatestUserMembershipSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(org_es_model.OrgAggregate, proj_es_model.ProjectAggregate).
LatestSequenceFilter(sequence.CurrentSequence), nil
}
func (m *UserMembership) Reduce(event *models.Event) (err error) {
switch event.AggregateType {
case org_es_model.OrgAggregate:
err = m.processOrg(event)
case proj_es_model.ProjectAggregate:
err = m.processProject(event)
}
return err
}
func (m *UserMembership) processOrg(event *models.Event) (err error) {
member := new(usr_es_model.UserMembershipView)
err = member.AppendEvent(event)
if err != nil {
return err
}
switch event.Type {
case org_es_model.OrgMemberAdded:
err = m.fillOrgDisplayName(member)
case org_es_model.OrgMemberChanged:
member, err = m.view.UserMembershipByIDs(member.UserID, event.AggregateID, event.AggregateID, usr_model.MemberTypeOrganisation)
if err != nil {
return err
}
err = member.AppendEvent(event)
case org_es_model.OrgMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, event.AggregateID, usr_model.MemberTypeOrganisation, event.Sequence)
case org_es_model.OrgChanged:
err = m.updateOrgDisplayName(event)
default:
return m.view.ProcessedUserMembershipSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutUserMembership(member, event.Sequence)
}
func (m *UserMembership) fillOrgDisplayName(member *usr_es_model.UserMembershipView) (err error) {
org, err := m.orgEvents.OrgByID(context.Background(), org_model.NewOrg(member.AggregateID))
if err != nil {
return err
}
member.DisplayName = org.Name
return nil
}
func (m *UserMembership) updateOrgDisplayName(event *models.Event) error {
org, err := m.orgEvents.OrgByID(context.Background(), org_model.NewOrg(event.AggregateID))
if err != nil {
return err
}
memberships, err := m.view.UserMembershipsByAggregateID(event.AggregateID)
if err != nil {
return err
}
for _, membership := range memberships {
membership.DisplayName = org.Name
}
return m.view.BulkPutUserMemberships(memberships, event.Sequence)
}
func (m *UserMembership) processProject(event *models.Event) (err error) {
member := new(usr_es_model.UserMembershipView)
err = member.AppendEvent(event)
if err != nil {
return err
}
switch event.Type {
case proj_es_model.ProjectMemberAdded, proj_es_model.ProjectGrantMemberAdded:
err = m.fillProjectDisplayName(member)
case proj_es_model.ProjectMemberChanged:
member, err = m.view.UserMembershipByIDs(member.UserID, event.AggregateID, event.AggregateID, usr_model.MemberTypeProject)
if err != nil {
return err
}
err = member.AppendEvent(event)
case proj_es_model.ProjectMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, event.AggregateID, usr_model.MemberTypeProject, event.Sequence)
case proj_es_model.ProjectGrantMemberChanged:
member, err = m.view.UserMembershipByIDs(member.UserID, event.AggregateID, member.ObjectID, usr_model.MemberTypeProjectGrant)
if err != nil {
return err
}
err = member.AppendEvent(event)
case proj_es_model.ProjectGrantMemberRemoved:
return m.view.DeleteUserMembership(member.UserID, event.AggregateID, member.ObjectID, usr_model.MemberTypeProjectGrant, event.Sequence)
case proj_es_model.ProjectChanged:
err = m.updateProjectDisplayName(event)
default:
return m.view.ProcessedUserMembershipSequence(event.Sequence)
}
if err != nil {
return err
}
return m.view.PutUserMembership(member, event.Sequence)
}
func (m *UserMembership) fillProjectDisplayName(member *usr_es_model.UserMembershipView) (err error) {
project, err := m.projectEvents.ProjectByID(context.Background(), member.AggregateID)
if err != nil {
return err
}
member.DisplayName = project.Name
return nil
}
func (m *UserMembership) updateProjectDisplayName(event *models.Event) error {
project, err := m.projectEvents.ProjectByID(context.Background(), event.AggregateID)
if err != nil {
return err
}
memberships, err := m.view.UserMembershipsByAggregateID(event.AggregateID)
if err != nil {
return err
}
for _, membership := range memberships {
membership.DisplayName = project.Name
}
return m.view.BulkPutUserMemberships(memberships, event.Sequence)
}
func (m *UserMembership) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-Ms3fj", "id", event.AggregateID).WithError(err).Warn("something went wrong in orgmember handler")
return spooler.HandleError(event, err, m.view.GetLatestUserMembershipFailedEvent, m.view.ProcessedUserMembershipFailedEvent, m.view.ProcessedUserMembershipSequence, m.errorCountUntilSkip)
}

View File

@@ -15,7 +15,7 @@ func (v *View) ApplicationByID(appID string) (*model.ApplicationView, error) {
return view.ApplicationByID(v.Db, applicationTable, appID)
}
func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, int, error) {
func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, uint64, error) {
return view.SearchApplications(v.Db, applicationTable, request)
}

View File

@@ -23,7 +23,7 @@ func (v *View) VerifiedOrgDomain(domain string) (*model.OrgDomainView, error) {
return view.VerifiedOrgDomain(v.Db, orgDomainTable, domain)
}
func (v *View) SearchOrgDomains(request *org_model.OrgDomainSearchRequest) ([]*model.OrgDomainView, int, error) {
func (v *View) SearchOrgDomains(request *org_model.OrgDomainSearchRequest) ([]*model.OrgDomainView, uint64, error) {
return view.SearchOrgDomains(v.Db, orgDomainTable, request)
}

View File

@@ -15,7 +15,7 @@ func (v *View) OrgMemberByIDs(orgID, userID string) (*model.OrgMemberView, error
return view.OrgMemberByIDs(v.Db, orgMemberTable, orgID, userID)
}
func (v *View) SearchOrgMembers(request *org_model.OrgMemberSearchRequest) ([]*model.OrgMemberView, int, error) {
func (v *View) SearchOrgMembers(request *org_model.OrgMemberSearchRequest) ([]*model.OrgMemberView, uint64, error) {
return view.SearchOrgMembers(v.Db, orgMemberTable, request)
}

View File

@@ -15,7 +15,7 @@ func (v *View) ProjectByID(projectID string) (*model.ProjectView, error) {
return view.ProjectByID(v.Db, projectTable, projectID)
}
func (v *View) SearchProjects(request *proj_model.ProjectViewSearchRequest) ([]*model.ProjectView, int, error) {
func (v *View) SearchProjects(request *proj_model.ProjectViewSearchRequest) ([]*model.ProjectView, uint64, error) {
return view.SearchProjects(v.Db, projectTable, request)
}

View File

@@ -27,7 +27,7 @@ func (v *View) ProjectGrantsByProjectIDAndRoleKey(projectID, key string) ([]*mod
return view.ProjectGrantsByProjectIDAndRoleKey(v.Db, grantedProjectTable, projectID, key)
}
func (v *View) SearchProjectGrants(request *proj_model.ProjectGrantViewSearchRequest) ([]*model.ProjectGrantView, int, error) {
func (v *View) SearchProjectGrants(request *proj_model.ProjectGrantViewSearchRequest) ([]*model.ProjectGrantView, uint64, error) {
return view.SearchProjectGrants(v.Db, grantedProjectTable, request)
}

View File

@@ -15,7 +15,7 @@ func (v *View) ProjectGrantMemberByIDs(projectID, userID string) (*model.Project
return view.ProjectGrantMemberByIDs(v.Db, projectGrantMemberTable, projectID, userID)
}
func (v *View) SearchProjectGrantMembers(request *proj_model.ProjectGrantMemberSearchRequest) ([]*model.ProjectGrantMemberView, int, error) {
func (v *View) SearchProjectGrantMembers(request *proj_model.ProjectGrantMemberSearchRequest) ([]*model.ProjectGrantMemberView, uint64, error) {
return view.SearchProjectGrantMembers(v.Db, projectGrantMemberTable, request)
}

View File

@@ -15,7 +15,7 @@ func (v *View) ProjectMemberByIDs(projectID, userID string) (*model.ProjectMembe
return view.ProjectMemberByIDs(v.Db, projectMemberTable, projectID, userID)
}
func (v *View) SearchProjectMembers(request *proj_model.ProjectMemberSearchRequest) ([]*model.ProjectMemberView, int, error) {
func (v *View) SearchProjectMembers(request *proj_model.ProjectMemberSearchRequest) ([]*model.ProjectMemberView, uint64, error) {
return view.SearchProjectMembers(v.Db, projectMemberTable, request)
}

View File

@@ -23,7 +23,7 @@ func (v *View) ResourceOwnerProjectRoles(projectID, resourceowner string) ([]*mo
return view.ResourceOwnerProjectRoles(v.Db, projectRoleTable, projectID, resourceowner)
}
func (v *View) SearchProjectRoles(request *proj_model.ProjectRoleSearchRequest) ([]*model.ProjectRoleView, int, error) {
func (v *View) SearchProjectRoles(request *proj_model.ProjectRoleSearchRequest) ([]*model.ProjectRoleView, uint64, error) {
return view.SearchProjectRoles(v.Db, projectRoleTable, request)
}

View File

@@ -15,7 +15,7 @@ func (v *View) UserByID(userID string) (*model.UserView, error) {
return view.UserByID(v.Db, userTable, userID)
}
func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserView, int, error) {
func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserView, uint64, error) {
return view.SearchUsers(v.Db, userTable, request)
}

View File

@@ -15,7 +15,7 @@ func (v *View) UserGrantByID(grantID string) (*model.UserGrantView, error) {
return view.UserGrantByID(v.Db, userGrantTable, grantID)
}
func (v *View) SearchUserGrants(request *grant_model.UserGrantSearchRequest) ([]*model.UserGrantView, int, error) {
func (v *View) SearchUserGrants(request *grant_model.UserGrantSearchRequest) ([]*model.UserGrantView, uint64, error) {
return view.SearchUserGrants(v.Db, userGrantTable, request)
}

View File

@@ -0,0 +1,64 @@
package view
import (
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
"github.com/caos/zitadel/internal/view/repository"
)
const (
userMembershipTable = "management.user_memberships"
)
func (v *View) UserMembershipByIDs(userID, aggregateID, objectID string, memberType usr_model.MemberType) (*model.UserMembershipView, error) {
return view.UserMembershipByIDs(v.Db, userMembershipTable, userID, aggregateID, objectID, memberType)
}
func (v *View) UserMembershipsByAggregateID(aggregateID string) ([]*model.UserMembershipView, error) {
return view.UserMembershipsByAggregateID(v.Db, userMembershipTable, aggregateID)
}
func (v *View) SearchUserMemberships(request *usr_model.UserMembershipSearchRequest) ([]*model.UserMembershipView, uint64, error) {
return view.SearchUserMemberships(v.Db, userMembershipTable, request)
}
func (v *View) PutUserMembership(membership *model.UserMembershipView, sequence uint64) error {
err := view.PutUserMembership(v.Db, userMembershipTable, membership)
if err != nil {
return err
}
return v.ProcessedUserMembershipSequence(sequence)
}
func (v *View) BulkPutUserMemberships(memberships []*model.UserMembershipView, sequence uint64) error {
err := view.PutUserMemberships(v.Db, userTable, memberships...)
if err != nil {
return err
}
return v.ProcessedUserMembershipSequence(sequence)
}
func (v *View) DeleteUserMembership(userID, aggregateID, objectID string, memberType usr_model.MemberType, eventSequence uint64) error {
err := view.DeleteUserMembership(v.Db, userMembershipTable, userID, aggregateID, objectID, memberType)
if err != nil {
return nil
}
return v.ProcessedUserMembershipSequence(eventSequence)
}
func (v *View) GetLatestUserMembershipSequence() (*repository.CurrentSequence, error) {
return v.latestSequence(userMembershipTable)
}
func (v *View) ProcessedUserMembershipSequence(eventSequence uint64) error {
return v.saveCurrentSequence(userMembershipTable, eventSequence)
}
func (v *View) GetLatestUserMembershipFailedEvent(sequence uint64) (*repository.FailedEvent, error) {
return v.latestFailedEvent(userMembershipTable, sequence)
}
func (v *View) ProcessedUserMembershipFailedEvent(failedEvent *repository.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@@ -37,4 +37,6 @@ type UserRepository interface {
AddressByID(ctx context.Context, userID string) (*model.Address, error)
ChangeAddress(ctx context.Context, address *model.Address) (*model.Address, error)
SearchUserMemberships(ctx context.Context, request *model.UserMembershipSearchRequest) (*model.UserMembershipSearchResponse, error)
}