mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:17:32 +00:00
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:
@@ -22,3 +22,7 @@ type Profile struct {
|
||||
func (p *Profile) IsValid() bool {
|
||||
return p.FirstName != "" && p.LastName != ""
|
||||
}
|
||||
|
||||
func (p *Profile) SetNamesAsDisplayname() {
|
||||
p.DisplayName = p.FirstName + " " + p.LastName
|
||||
}
|
||||
|
88
internal/user/model/user_membership_view.go
Normal file
88
internal/user/model/user_membership_view.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
)
|
||||
|
||||
type UserMembershipView struct {
|
||||
UserID string
|
||||
MemberType MemberType
|
||||
AggregateID string
|
||||
//ObjectID differs from aggregate id if obejct is sub of an aggregate
|
||||
ObjectID string
|
||||
|
||||
Roles []string
|
||||
DisplayName string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type MemberType int32
|
||||
|
||||
const (
|
||||
MemberTypeUnspecified MemberType = iota
|
||||
MemberTypeOrganisation
|
||||
MemberTypeProject
|
||||
MemberTypeProjectGrant
|
||||
)
|
||||
|
||||
type UserMembershipSearchRequest struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
SortingColumn UserMembershipSearchKey
|
||||
Asc bool
|
||||
Queries []*UserMembershipSearchQuery
|
||||
}
|
||||
|
||||
type UserMembershipSearchKey int32
|
||||
|
||||
const (
|
||||
UserMembershipSearchKeyUnspecified UserMembershipSearchKey = iota
|
||||
UserMembershipSearchKeyUserID
|
||||
UserMembershipSearchKeyMemberType
|
||||
UserMembershipSearchKeyAggregateID
|
||||
UserMembershipSearchKeyObjectID
|
||||
UserMembershipSearchKeyResourceOwner
|
||||
)
|
||||
|
||||
type UserMembershipSearchQuery struct {
|
||||
Key UserMembershipSearchKey
|
||||
Method model.SearchMethod
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type UserMembershipSearchResponse struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
TotalResult uint64
|
||||
Result []*UserMembershipView
|
||||
Sequence uint64
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (r *UserMembershipSearchRequest) EnsureLimit(limit uint64) {
|
||||
if r.Limit == 0 || r.Limit > limit {
|
||||
r.Limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserMembershipSearchRequest) GetSearchQuery(key UserMembershipSearchKey) (int, *UserMembershipSearchQuery) {
|
||||
for i, q := range r.Queries {
|
||||
if q.Key == key {
|
||||
return i, q
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (r *UserMembershipSearchRequest) AppendResourceOwnerQuery(orgID string) {
|
||||
r.Queries = append(r.Queries, &UserMembershipSearchQuery{Key: UserMembershipSearchKeyResourceOwner, Method: model.SearchMethodEquals, Value: orgID})
|
||||
}
|
||||
|
||||
func (r *UserMembershipSearchRequest) AppendUserIDQuery(userID string) {
|
||||
r.Queries = append(r.Queries, &UserMembershipSearchQuery{Key: UserMembershipSearchKeyUserID, Method: model.SearchMethodEquals, Value: userID})
|
||||
}
|
@@ -611,6 +611,7 @@ func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model.Profile) (*usr_model.Profile, error) {
|
||||
profile.SetNamesAsDisplayname()
|
||||
if !profile.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d82i3", "Errors.User.ProfileInvalid")
|
||||
}
|
||||
@@ -618,6 +619,7 @@ func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoExisting := model.UserFromModel(existing)
|
||||
repoNew := model.ProfileFromModel(profile)
|
||||
|
||||
|
139
internal/user/repository/view/model/user_membership.go
Normal file
139
internal/user/repository/view/model/user_membership.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
proj_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/lib/pq"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserMembershipKeyUserID = "user_id"
|
||||
UserMembershipKeyAggregateID = "aggregate_id"
|
||||
UserMembershipKeyObjectID = "object_id"
|
||||
UserMembershipKeyResourceOwner = "resource_owner"
|
||||
UserMembershipKeyMemberType = "member_type"
|
||||
)
|
||||
|
||||
type UserMembershipView struct {
|
||||
UserID string `json:"-" gorm:"column:user_id;primary_key"`
|
||||
MemberType int32 `json:"-" gorm:"column:member_type;primary_key"`
|
||||
AggregateID string `json:"-" gorm:"column:aggregate_id;primary_key"`
|
||||
ObjectID string `json:"-" gorm:"column:object_id;primary_key"`
|
||||
|
||||
Roles pq.StringArray `json:"-" gorm:"column:roles"`
|
||||
DisplayName string `json:"-" gorm:"column:display_name"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func UserMembershipFromModel(membership *model.UserMembershipView) *UserMembershipView {
|
||||
return &UserMembershipView{
|
||||
UserID: membership.UserID,
|
||||
MemberType: int32(membership.MemberType),
|
||||
AggregateID: membership.AggregateID,
|
||||
ObjectID: membership.ObjectID,
|
||||
Roles: membership.Roles,
|
||||
DisplayName: membership.DisplayName,
|
||||
ChangeDate: membership.ChangeDate,
|
||||
CreationDate: membership.CreationDate,
|
||||
ResourceOwner: membership.ResourceOwner,
|
||||
Sequence: membership.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func UserMembershipToModel(membership *UserMembershipView) *model.UserMembershipView {
|
||||
return &model.UserMembershipView{
|
||||
UserID: membership.UserID,
|
||||
MemberType: model.MemberType(membership.MemberType),
|
||||
AggregateID: membership.AggregateID,
|
||||
ObjectID: membership.ObjectID,
|
||||
Roles: membership.Roles,
|
||||
DisplayName: membership.DisplayName,
|
||||
ChangeDate: membership.ChangeDate,
|
||||
CreationDate: membership.CreationDate,
|
||||
ResourceOwner: membership.ResourceOwner,
|
||||
Sequence: membership.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func UserMembershipsToModel(memberships []*UserMembershipView) []*model.UserMembershipView {
|
||||
result := make([]*model.UserMembershipView, len(memberships))
|
||||
for i, m := range memberships {
|
||||
result[i] = UserMembershipToModel(m)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (u *UserMembershipView) AppendEvent(event *models.Event) (err error) {
|
||||
u.ChangeDate = event.CreationDate
|
||||
u.Sequence = event.Sequence
|
||||
|
||||
switch event.Type {
|
||||
case org_es_model.OrgMemberAdded:
|
||||
u.setRootData(event, model.MemberTypeOrganisation)
|
||||
err = u.setOrgMemberData(event)
|
||||
case org_es_model.OrgMemberChanged:
|
||||
err = u.setOrgMemberData(event)
|
||||
case proj_es_model.ProjectMemberAdded:
|
||||
u.setRootData(event, model.MemberTypeProject)
|
||||
err = u.setProjectMemberData(event)
|
||||
case proj_es_model.ProjectMemberChanged:
|
||||
err = u.setProjectMemberData(event)
|
||||
case proj_es_model.ProjectGrantMemberAdded:
|
||||
u.setRootData(event, model.MemberTypeProjectGrant)
|
||||
err = u.setProjectMemberData(event)
|
||||
case proj_es_model.ProjectGrantMemberChanged:
|
||||
err = u.setProjectMemberData(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UserMembershipView) setRootData(event *models.Event, memberType model.MemberType) {
|
||||
u.CreationDate = event.CreationDate
|
||||
u.AggregateID = event.AggregateID
|
||||
u.ObjectID = event.AggregateID
|
||||
u.ResourceOwner = event.ResourceOwner
|
||||
u.MemberType = int32(memberType)
|
||||
}
|
||||
|
||||
func (u *UserMembershipView) setOrgMemberData(event *models.Event) error {
|
||||
member := new(org_es_model.OrgMember)
|
||||
if err := json.Unmarshal(event.Data, member); err != nil {
|
||||
logging.Log("MODEL-Lps0e").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
|
||||
}
|
||||
u.UserID = member.UserID
|
||||
u.Roles = member.Roles
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserMembershipView) setProjectMemberData(event *models.Event) error {
|
||||
member := new(proj_es_model.ProjectMember)
|
||||
if err := json.Unmarshal(event.Data, member); err != nil {
|
||||
logging.Log("MODEL-Esu8k").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
|
||||
}
|
||||
u.UserID = member.UserID
|
||||
u.Roles = member.Roles
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserMembershipView) setProjectGrantMemberData(event *models.Event) error {
|
||||
member := new(proj_es_model.ProjectGrantMember)
|
||||
if err := json.Unmarshal(event.Data, member); err != nil {
|
||||
logging.Log("MODEL-MCn8s").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
|
||||
}
|
||||
u.UserID = member.UserID
|
||||
u.ObjectID = member.GrantID
|
||||
u.Roles = member.Roles
|
||||
return nil
|
||||
}
|
67
internal/user/repository/view/model/user_membership_query.go
Normal file
67
internal/user/repository/view/model/user_membership_query.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
type UserMembershipSearchRequest usr_model.UserMembershipSearchRequest
|
||||
type UserMembershipSearchQuery usr_model.UserMembershipSearchQuery
|
||||
type UserMembershipSearchKey usr_model.UserMembershipSearchKey
|
||||
|
||||
func (req UserMembershipSearchRequest) GetLimit() uint64 {
|
||||
return req.Limit
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchRequest) GetOffset() uint64 {
|
||||
return req.Offset
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchRequest) GetSortingColumn() repository.ColumnKey {
|
||||
if req.SortingColumn == usr_model.UserMembershipSearchKeyUnspecified {
|
||||
return nil
|
||||
}
|
||||
return UserMembershipSearchKey(req.SortingColumn)
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchRequest) GetAsc() bool {
|
||||
return req.Asc
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchRequest) GetQueries() []repository.SearchQuery {
|
||||
result := make([]repository.SearchQuery, len(req.Queries))
|
||||
for i, q := range req.Queries {
|
||||
result[i] = UserMembershipSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchQuery) GetKey() repository.ColumnKey {
|
||||
return UserMembershipSearchKey(req.Key)
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchQuery) GetMethod() global_model.SearchMethod {
|
||||
return req.Method
|
||||
}
|
||||
|
||||
func (req UserMembershipSearchQuery) GetValue() interface{} {
|
||||
return req.Value
|
||||
}
|
||||
|
||||
func (key UserMembershipSearchKey) ToColumnName() string {
|
||||
switch usr_model.UserMembershipSearchKey(key) {
|
||||
case usr_model.UserMembershipSearchKeyUserID:
|
||||
return UserMembershipKeyUserID
|
||||
case usr_model.UserMembershipSearchKeyResourceOwner:
|
||||
return UserMembershipKeyResourceOwner
|
||||
case usr_model.UserMembershipSearchKeyMemberType:
|
||||
return UserMembershipKeyMemberType
|
||||
case usr_model.UserMembershipSearchKeyAggregateID:
|
||||
return UserMembershipKeyAggregateID
|
||||
case usr_model.UserMembershipSearchKeyObjectID:
|
||||
return UserMembershipKeyObjectID
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
@@ -59,7 +59,7 @@ func UsersByOrgID(db *gorm.DB, table, orgID string) ([]*model.UserView, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
func SearchUsers(db *gorm.DB, table string, req *usr_model.UserSearchRequest) ([]*model.UserView, int, error) {
|
||||
func SearchUsers(db *gorm.DB, table string, req *usr_model.UserSearchRequest) ([]*model.UserView, uint64, error) {
|
||||
users := make([]*model.UserView, 0)
|
||||
query := repository.PrepareSearchQuery(table, model.UserSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
||||
count, err := query(db, &users)
|
||||
|
70
internal/user/repository/view/usermembership_view.go
Normal file
70
internal/user/repository/view/usermembership_view.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
"github.com/caos/zitadel/internal/view/repository"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
func UserMembershipByIDs(db *gorm.DB, table, userID, aggregateID, objectID string, membertype usr_model.MemberType) (*model.UserMembershipView, error) {
|
||||
memberships := new(model.UserMembershipView)
|
||||
userIDQuery := &model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyUserID, Value: userID, Method: global_model.SearchMethodEquals}
|
||||
aggregateIDQuery := &model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyAggregateID, Value: aggregateID, Method: global_model.SearchMethodEquals}
|
||||
objectIDQuery := &model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyObjectID, Value: objectID, Method: global_model.SearchMethodEquals}
|
||||
memberTypeQuery := &model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyMemberType, Value: int32(membertype), Method: global_model.SearchMethodEquals}
|
||||
|
||||
query := repository.PrepareGetByQuery(table, userIDQuery, aggregateIDQuery, objectIDQuery, memberTypeQuery)
|
||||
err := query(db, memberships)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "VIEW-sj8Sw", "Errors.UserMembership.NotFound")
|
||||
}
|
||||
return memberships, err
|
||||
}
|
||||
|
||||
func UserMembershipsByAggregateID(db *gorm.DB, table, aggregateID string) ([]*model.UserMembershipView, error) {
|
||||
memberships := make([]*model.UserMembershipView, 0)
|
||||
aggregateIDQuery := &usr_model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyAggregateID, Value: aggregateID, Method: global_model.SearchMethodEquals}
|
||||
query := repository.PrepareSearchQuery(table, model.UserMembershipSearchRequest{
|
||||
Queries: []*usr_model.UserMembershipSearchQuery{aggregateIDQuery},
|
||||
})
|
||||
_, err := query(db, memberships)
|
||||
return memberships, err
|
||||
}
|
||||
|
||||
func SearchUserMemberships(db *gorm.DB, table string, req *usr_model.UserMembershipSearchRequest) ([]*model.UserMembershipView, uint64, error) {
|
||||
users := make([]*model.UserMembershipView, 0)
|
||||
query := repository.PrepareSearchQuery(table, model.UserMembershipSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
||||
count, err := query(db, &users)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return users, count, nil
|
||||
}
|
||||
|
||||
func PutUserMemberships(db *gorm.DB, table string, users ...*model.UserMembershipView) error {
|
||||
save := repository.PrepareBulkSave(table)
|
||||
u := make([]interface{}, len(users))
|
||||
for i, user := range users {
|
||||
u[i] = user
|
||||
}
|
||||
return save(db, u...)
|
||||
}
|
||||
|
||||
func PutUserMembership(db *gorm.DB, table string, user *model.UserMembershipView) error {
|
||||
save := repository.PrepareSave(table)
|
||||
return save(db, user)
|
||||
}
|
||||
|
||||
func DeleteUserMembership(db *gorm.DB, table, userID, aggregateID, objectID string, membertype usr_model.MemberType) error {
|
||||
delete := repository.PrepareDeleteByKeys(table,
|
||||
repository.Key{Key: model.UserMembershipSearchKey(usr_model.UserMembershipSearchKeyUserID), Value: userID},
|
||||
repository.Key{Key: model.UserMembershipSearchKey(usr_model.UserMembershipSearchKeyAggregateID), Value: aggregateID},
|
||||
repository.Key{Key: model.UserMembershipSearchKey(usr_model.UserMembershipSearchKeyObjectID), Value: objectID},
|
||||
repository.Key{Key: model.UserMembershipSearchKey(usr_model.UserMembershipSearchKeyMemberType), Value: membertype},
|
||||
)
|
||||
return delete(db)
|
||||
}
|
Reference in New Issue
Block a user