mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-07 20:17:43 +00:00
feat(auth): My user changes (#318)
* fix: project by id loads project from view and from eventstore * fix: correct search key for role * feat(auth): my user changes * fix: improve error handling in change converters * fix: log-id
This commit is contained in:
parent
4f3631acbb
commit
cf7a906023
@ -2,6 +2,7 @@ package eventstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/caos/zitadel/internal/eventstore"
|
"github.com/caos/zitadel/internal/eventstore"
|
||||||
"github.com/caos/zitadel/internal/eventstore/sdk"
|
"github.com/caos/zitadel/internal/eventstore/sdk"
|
||||||
@ -258,6 +259,21 @@ func (repo *UserRepo) UserByID(ctx context.Context, id string) (*model.UserView,
|
|||||||
return usr_view_model.UserToModel(&userCopy), nil
|
return usr_view_model.UserToModel(&userCopy), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error) {
|
||||||
|
changes, err := repo.UserEvents.UserChanges(ctx, auth.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, change := range changes.Changes {
|
||||||
|
change.ModifierName = change.ModifierId
|
||||||
|
user, _ := repo.UserEvents.UserByID(ctx, change.ModifierId)
|
||||||
|
if user != nil {
|
||||||
|
change.ModifierName = user.DisplayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkIDs(ctx context.Context, obj es_models.ObjectRoot) error {
|
func checkIDs(ctx context.Context, obj es_models.ObjectRoot) error {
|
||||||
if obj.AggregateID != auth.GetCtxData(ctx).UserID {
|
if obj.AggregateID != auth.GetCtxData(ctx).UserID {
|
||||||
return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user")
|
return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user")
|
||||||
|
@ -2,6 +2,9 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
@ -22,8 +25,6 @@ import (
|
|||||||
usr_es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
usr_es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||||
grant_es_model "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
|
grant_es_model "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
|
||||||
view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
|
view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserGrant struct {
|
type UserGrant struct {
|
||||||
@ -171,7 +172,6 @@ func (u *UserGrant) processOrg(event *models.Event) (err error) {
|
|||||||
default:
|
default:
|
||||||
return u.view.ProcessedUserGrantSequence(event.Sequence)
|
return u.view.ProcessedUserGrantSequence(event.Sequence)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserGrant) processIamMember(event *models.Event, rolePrefix string, suffix bool) error {
|
func (u *UserGrant) processIamMember(event *models.Event, rolePrefix string, suffix bool) error {
|
||||||
@ -341,6 +341,6 @@ func (u *UserGrant) fillOrgData(grant *view_model.UserGrantView, org *org_model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserGrant) OnError(event *models.Event, err error) error {
|
func (u *UserGrant) OnError(event *models.Event, err error) error {
|
||||||
logging.LogWithFields("SPOOL-8is4s", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler")
|
logging.LogWithFields("SPOOL-UZmc7", "id", event.AggregateID).WithError(err).Warn("something went wrong in user grant handler")
|
||||||
return spooler.HandleError(event, err, u.view.GetLatestUserGrantFailedEvent, u.view.ProcessedUserGrantFailedEvent, u.view.ProcessedUserGrantSequence, u.errorCountUntilSkip)
|
return spooler.HandleError(event, err, u.view.GetLatestUserGrantFailedEvent, u.view.ProcessedUserGrantFailedEvent, u.view.ProcessedUserGrantSequence, u.errorCountUntilSkip)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
org_model "github.com/caos/zitadel/internal/org/model"
|
org_model "github.com/caos/zitadel/internal/org/model"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/user/model"
|
"github.com/caos/zitadel/internal/user/model"
|
||||||
@ -53,4 +54,6 @@ type myUserRepo interface {
|
|||||||
AddMyMfaOTP(ctx context.Context) (*model.OTP, error)
|
AddMyMfaOTP(ctx context.Context) (*model.OTP, error)
|
||||||
VerifyMyMfaOTPSetup(ctx context.Context, code string) error
|
VerifyMyMfaOTPSetup(ctx context.Context, code string) error
|
||||||
RemoveMyMfaOTP(ctx context.Context) error
|
RemoveMyMfaOTP(ctx context.Context) error
|
||||||
|
|
||||||
|
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||||
@ -14,8 +17,6 @@ import (
|
|||||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||||
proj_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
proj_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
||||||
view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
|
view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserGrant struct {
|
type UserGrant struct {
|
||||||
@ -223,6 +224,6 @@ func (u *UserGrant) setIamProjectID() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserGrant) OnError(event *models.Event, err error) error {
|
func (u *UserGrant) OnError(event *models.Event, err error) error {
|
||||||
logging.LogWithFields("SPOOL-8is4s", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler")
|
logging.LogWithFields("SPOOL-VcVoJ", "id", event.AggregateID).WithError(err).Warn("something went wrong in user grant handler")
|
||||||
return spooler.HandleError(event, err, u.view.GetLatestUserGrantFailedEvent, u.view.ProcessedUserGrantFailedEvent, u.view.ProcessedUserGrantSequence, u.errorCountUntilSkip)
|
return spooler.HandleError(event, err, u.view.GetLatestUserGrantFailedEvent, u.view.ProcessedUserGrantFailedEvent, u.view.ProcessedUserGrantSequence, u.errorCountUntilSkip)
|
||||||
}
|
}
|
||||||
|
13
internal/cache/bigcache/cache.go
vendored
13
internal/cache/bigcache/cache.go
vendored
@ -3,12 +3,11 @@ package bigcache
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"github.com/caos/logging"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/errors"
|
|
||||||
|
|
||||||
a_cache "github.com/allegro/bigcache"
|
a_cache "github.com/allegro/bigcache"
|
||||||
|
"github.com/caos/logging"
|
||||||
|
"github.com/caos/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bigcache struct {
|
type Bigcache struct {
|
||||||
@ -30,19 +29,19 @@ func NewBigcache(c *Config) (*Bigcache, error) {
|
|||||||
|
|
||||||
func (c *Bigcache) Set(key string, object interface{}) error {
|
func (c *Bigcache) Set(key string, object interface{}) error {
|
||||||
if key == "" || reflect.ValueOf(object).IsNil() {
|
if key == "" || reflect.ValueOf(object).IsNil() {
|
||||||
return errors.ThrowInvalidArgument(nil, "FASTC-du73s", "key or value should not be empty")
|
return errors.ThrowInvalidArgument(nil, "BIGCA-du73s", "key or value should not be empty")
|
||||||
}
|
}
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
enc := gob.NewEncoder(&b)
|
enc := gob.NewEncoder(&b)
|
||||||
if err := enc.Encode(object); err != nil {
|
if err := enc.Encode(object); err != nil {
|
||||||
return errors.ThrowInvalidArgument(err, "FASTC-RUyxI", "unable to encode object")
|
return errors.ThrowInvalidArgument(err, "BIGCA-RUyxI", "unable to encode object")
|
||||||
}
|
}
|
||||||
return c.cache.Set(key, b.Bytes())
|
return c.cache.Set(key, b.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Bigcache) Get(key string, ptrToObject interface{}) error {
|
func (c *Bigcache) Get(key string, ptrToObject interface{}) error {
|
||||||
if key == "" || reflect.ValueOf(ptrToObject).IsNil() {
|
if key == "" || reflect.ValueOf(ptrToObject).IsNil() {
|
||||||
return errors.ThrowInvalidArgument(nil, "FASTC-dksoe", "key or value should not be empty")
|
return errors.ThrowInvalidArgument(nil, "BIGCA-dksoe", "key or value should not be empty")
|
||||||
}
|
}
|
||||||
value, err := c.cache.Get(key)
|
value, err := c.cache.Get(key)
|
||||||
if err == a_cache.ErrEntryNotFound {
|
if err == a_cache.ErrEntryNotFound {
|
||||||
@ -61,7 +60,7 @@ func (c *Bigcache) Get(key string, ptrToObject interface{}) error {
|
|||||||
|
|
||||||
func (c *Bigcache) Delete(key string) error {
|
func (c *Bigcache) Delete(key string) error {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return errors.ThrowInvalidArgument(nil, "FASTC-clsi2", "key should not be empty")
|
return errors.ThrowInvalidArgument(nil, "BIGCA-clsi2", "key should not be empty")
|
||||||
}
|
}
|
||||||
return c.cache.Delete(key)
|
return c.cache.Delete(key)
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func (p *ProjectGrant) Reduce(event *models.Event) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.updateExistingProjects(project)
|
return p.updateExistingProjects(project)
|
||||||
case es_model.ProjectGrantAdded:
|
case es_model.ProjectGrantAdded:
|
||||||
err = grantedProject.AppendEvent(event)
|
err = grantedProject.AppendEvent(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,19 +107,25 @@ func (p *ProjectGrant) getProject(projectID string) (*proj_model.Project, error)
|
|||||||
return p.projectEvents.ProjectByID(context.Background(), projectID)
|
return p.projectEvents.ProjectByID(context.Background(), projectID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectGrant) updateExistingProjects(project *view_model.ProjectView) {
|
func (p *ProjectGrant) updateExistingProjects(project *view_model.ProjectView) error {
|
||||||
projects, err := p.view.ProjectGrantsByProjectID(project.ProjectID)
|
projectGrants, err := p.view.ProjectGrantsByProjectID(project.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.LogWithFields("SPOOL-los03", "id", project.ProjectID).WithError(err).Warn("could not update existing projects")
|
logging.LogWithFields("SPOOL-los03", "id", project.ProjectID).WithError(err).Warn("could not update existing projects")
|
||||||
}
|
}
|
||||||
for _, existing := range projects {
|
for _, existing := range projectGrants {
|
||||||
existing.Name = project.Name
|
existing.Name = project.Name
|
||||||
err := p.view.PutProjectGrant(existing)
|
err := p.view.PutProjectGrant(existing)
|
||||||
logging.LogWithFields("SPOOL-sjwi3", "id", existing.ProjectID).WithError(err).Warn("could not update existing project")
|
if err != nil {
|
||||||
|
logging.LogWithFields("SPOOL-sjwi3", "id", existing.ProjectID).WithError(err).Warn("could not update existing project")
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return p.view.ProcessedProjectGrantSequence(project.Sequence)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProjectGrant) OnError(event *models.Event, err error) error {
|
func (p *ProjectGrant) OnError(event *models.Event, err error) error {
|
||||||
logging.LogWithFields("SPOOL-is8wa", "id", event.AggregateID).WithError(err).Warn("something went wrong in granted projecthandler")
|
logging.LogWithFields("SPOOL-is8wa", "id", event.AggregateID).WithError(err).Warn("something went wrong in granted projecthandler")
|
||||||
return spooler.HandleError(event, err, p.view.GetLatestProjectGrantFailedEvent, p.view.ProcessedProjectGrantFailedEvent, p.view.ProcessedProjectGrantSequence, p.errorCountUntilSkip)
|
return spooler.HandleError(event, err, p.view.GetLatestProjectGrantFailedEvent, p.view.ProcessedProjectGrantFailedEvent, p.view.ProcessedProjectGrantSequence, p.errorCountUntilSkip)
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ type PolicyCache struct {
|
|||||||
|
|
||||||
func StartCache(conf *config.CacheConfig) (*PolicyCache, error) {
|
func StartCache(conf *config.CacheConfig) (*PolicyCache, error) {
|
||||||
policyCache, err := conf.Config.NewCache()
|
policyCache, err := conf.Config.NewCache()
|
||||||
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create policy cache")
|
logging.Log("EVENT-L7ZcH").OnError(err).Panic("unable to create policy cache")
|
||||||
|
|
||||||
return &PolicyCache{policyCache: policyCache}, nil
|
return &PolicyCache{policyCache: policyCache}, nil
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ func StartCache(conf *config.CacheConfig) (*PolicyCache, error) {
|
|||||||
func (c *PolicyCache) getPolicy(id string) (policy *PasswordComplexityPolicy) {
|
func (c *PolicyCache) getPolicy(id string) (policy *PasswordComplexityPolicy) {
|
||||||
policy = &PasswordComplexityPolicy{ObjectRoot: models.ObjectRoot{AggregateID: id}}
|
policy = &PasswordComplexityPolicy{ObjectRoot: models.ObjectRoot{AggregateID: id}}
|
||||||
if err := c.policyCache.Get(id, policy); err != nil {
|
if err := c.policyCache.Get(id, policy); err != nil {
|
||||||
logging.Log("EVENT-4eTZh").WithError(err).Debug("error in getting cache")
|
logging.Log("EVENT-tkUue").WithError(err).Debug("error in getting cache")
|
||||||
}
|
}
|
||||||
return policy
|
return policy
|
||||||
}
|
}
|
||||||
@ -29,6 +29,6 @@ func (c *PolicyCache) getPolicy(id string) (policy *PasswordComplexityPolicy) {
|
|||||||
func (c *PolicyCache) cachePolicy(policy *PasswordComplexityPolicy) {
|
func (c *PolicyCache) cachePolicy(policy *PasswordComplexityPolicy) {
|
||||||
err := c.policyCache.Set(policy.AggregateID, policy)
|
err := c.policyCache.Set(policy.AggregateID, policy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Log("EVENT-ThnBb").WithError(err).Debug("error in setting policy cache")
|
logging.Log("EVENT-DVcpF").WithError(err).Debug("error in setting policy cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ type ProjectCache struct {
|
|||||||
|
|
||||||
func StartCache(conf *config.CacheConfig) (*ProjectCache, error) {
|
func StartCache(conf *config.CacheConfig) (*ProjectCache, error) {
|
||||||
projectCache, err := conf.Config.NewCache()
|
projectCache, err := conf.Config.NewCache()
|
||||||
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create project cache")
|
logging.Log("EVENT-CsHdo").OnError(err).Panic("unable to create project cache")
|
||||||
|
|
||||||
return &ProjectCache{projectCache: projectCache}, nil
|
return &ProjectCache{projectCache: projectCache}, nil
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ func StartCache(conf *config.CacheConfig) (*ProjectCache, error) {
|
|||||||
func (c *ProjectCache) getProject(ID string) (project *model.Project) {
|
func (c *ProjectCache) getProject(ID string) (project *model.Project) {
|
||||||
project = &model.Project{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
project = &model.Project{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
||||||
if err := c.projectCache.Get(ID, project); err != nil {
|
if err := c.projectCache.Get(ID, project); err != nil {
|
||||||
logging.Log("EVENT-4eTZh").WithError(err).Debug("error in getting cache")
|
logging.Log("EVENT-tMydV").WithError(err).Debug("error in getting cache")
|
||||||
}
|
}
|
||||||
return project
|
return project
|
||||||
}
|
}
|
||||||
@ -30,6 +30,6 @@ func (c *ProjectCache) getProject(ID string) (project *model.Project) {
|
|||||||
func (c *ProjectCache) cacheProject(project *model.Project) {
|
func (c *ProjectCache) cacheProject(project *model.Project) {
|
||||||
err := c.projectCache.Set(project.AggregateID, project)
|
err := c.projectCache.Set(project.AggregateID, project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Log("EVENT-ThnBb").WithError(err).Debug("error in setting project cache")
|
logging.Log("EVENT-3wKzj").WithError(err).Debug("error in setting project cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,58 +9,51 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ProjectGrantByProjectAndOrg(db *gorm.DB, table, projectID, orgID string) (*model.ProjectGrantView, error) {
|
func ProjectGrantByProjectAndOrg(db *gorm.DB, table, projectID, orgID string) (*model.ProjectGrantView, error) {
|
||||||
project := new(model.ProjectGrantView)
|
projectGrant := new(model.ProjectGrantView)
|
||||||
|
|
||||||
projectIDQuery := model.ProjectGrantSearchQuery{Key: proj_model.GrantedProjectSearchKeyProjectID, Value: projectID, Method: global_model.SearchMethodEquals}
|
projectIDQuery := model.ProjectGrantSearchQuery{Key: proj_model.GrantedProjectSearchKeyProjectID, Value: projectID, Method: global_model.SearchMethodEquals}
|
||||||
orgIDQuery := model.ProjectGrantSearchQuery{Key: proj_model.GrantedProjectSearchKeyOrgID, Value: orgID, Method: global_model.SearchMethodEquals}
|
orgIDQuery := model.ProjectGrantSearchQuery{Key: proj_model.GrantedProjectSearchKeyOrgID, Value: orgID, Method: global_model.SearchMethodEquals}
|
||||||
query := repository.PrepareGetByQuery(table, projectIDQuery, orgIDQuery)
|
query := repository.PrepareGetByQuery(table, projectIDQuery, orgIDQuery)
|
||||||
err := query(db, project)
|
err := query(db, projectGrant)
|
||||||
return project, err
|
return projectGrant, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectGrantByID(db *gorm.DB, table, grantID string) (*model.ProjectGrantView, error) {
|
func ProjectGrantByID(db *gorm.DB, table, grantID string) (*model.ProjectGrantView, error) {
|
||||||
project := new(model.ProjectGrantView)
|
projectGrant := new(model.ProjectGrantView)
|
||||||
grantIDQuery := model.ProjectGrantSearchQuery{Key: proj_model.GrantedProjectSearchKeyGrantID, Value: grantID, Method: global_model.SearchMethodEquals}
|
grantIDQuery := model.ProjectGrantSearchQuery{Key: proj_model.GrantedProjectSearchKeyGrantID, Value: grantID, Method: global_model.SearchMethodEquals}
|
||||||
query := repository.PrepareGetByQuery(table, grantIDQuery)
|
query := repository.PrepareGetByQuery(table, grantIDQuery)
|
||||||
err := query(db, project)
|
err := query(db, projectGrant)
|
||||||
return project, err
|
return projectGrant, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectGrantsByProjectID(db *gorm.DB, table, projectID string) ([]*model.ProjectGrantView, error) {
|
func ProjectGrantsByProjectID(db *gorm.DB, table, projectID string) ([]*model.ProjectGrantView, error) {
|
||||||
projects := make([]*model.ProjectGrantView, 0)
|
projectGrants := make([]*model.ProjectGrantView, 0)
|
||||||
queries := []*proj_model.ProjectGrantViewSearchQuery{
|
queries := []*proj_model.ProjectGrantViewSearchQuery{
|
||||||
&proj_model.ProjectGrantViewSearchQuery{Key: proj_model.GrantedProjectSearchKeyProjectID, Value: projectID, Method: global_model.SearchMethodEquals},
|
{Key: proj_model.GrantedProjectSearchKeyProjectID, Value: projectID, Method: global_model.SearchMethodEquals},
|
||||||
}
|
}
|
||||||
query := repository.PrepareSearchQuery(table, model.ProjectGrantSearchRequest{Queries: queries})
|
query := repository.PrepareSearchQuery(table, model.ProjectGrantSearchRequest{Queries: queries})
|
||||||
_, err := query(db, &projects)
|
_, err := query(db, &projectGrants)
|
||||||
if err != nil {
|
return projectGrants, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return projects, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectGrantsByProjectIDAndRoleKey(db *gorm.DB, table, projectID, roleKey string) ([]*model.ProjectGrantView, error) {
|
func ProjectGrantsByProjectIDAndRoleKey(db *gorm.DB, table, projectID, roleKey string) ([]*model.ProjectGrantView, error) {
|
||||||
projects := make([]*model.ProjectGrantView, 0)
|
projectGrants := make([]*model.ProjectGrantView, 0)
|
||||||
queries := []*proj_model.ProjectGrantViewSearchQuery{
|
queries := []*proj_model.ProjectGrantViewSearchQuery{
|
||||||
&proj_model.ProjectGrantViewSearchQuery{Key: proj_model.GrantedProjectSearchKeyProjectID, Value: projectID, Method: global_model.SearchMethodEquals},
|
{Key: proj_model.GrantedProjectSearchKeyProjectID, Value: projectID, Method: global_model.SearchMethodEquals},
|
||||||
&proj_model.ProjectGrantViewSearchQuery{Key: proj_model.GrantedProjectSearchKeyRoleKeys, Value: roleKey, Method: global_model.SearchMethodListContains},
|
{Key: proj_model.GrantedProjectSearchKeyRoleKeys, Value: roleKey, Method: global_model.SearchMethodListContains},
|
||||||
}
|
}
|
||||||
query := repository.PrepareSearchQuery(table, model.ProjectGrantSearchRequest{Queries: queries})
|
query := repository.PrepareSearchQuery(table, model.ProjectGrantSearchRequest{Queries: queries})
|
||||||
_, err := query(db, &projects)
|
_, err := query(db, &projectGrants)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return projectGrants, err
|
||||||
}
|
|
||||||
return projects, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchProjectGrants(db *gorm.DB, table string, req *proj_model.ProjectGrantViewSearchRequest) ([]*model.ProjectGrantView, int, error) {
|
func SearchProjectGrants(db *gorm.DB, table string, req *proj_model.ProjectGrantViewSearchRequest) ([]*model.ProjectGrantView, int, error) {
|
||||||
projects := make([]*model.ProjectGrantView, 0)
|
projectGrants := make([]*model.ProjectGrantView, 0)
|
||||||
query := repository.PrepareSearchQuery(table, model.ProjectGrantSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
query := repository.PrepareSearchQuery(table, model.ProjectGrantSearchRequest{Limit: req.Limit, Offset: req.Offset, Queries: req.Queries})
|
||||||
count, err := query(db, &projects)
|
count, err := query(db, &projectGrants)
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
return projectGrants, count, err
|
||||||
}
|
|
||||||
return projects, count, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PutProjectGrant(db *gorm.DB, table string, project *model.ProjectGrantView) error {
|
func PutProjectGrant(db *gorm.DB, table string, project *model.ProjectGrantView) error {
|
||||||
|
@ -14,7 +14,7 @@ type UserCache struct {
|
|||||||
|
|
||||||
func StartCache(conf *config.CacheConfig) (*UserCache, error) {
|
func StartCache(conf *config.CacheConfig) (*UserCache, error) {
|
||||||
userCache, err := conf.Config.NewCache()
|
userCache, err := conf.Config.NewCache()
|
||||||
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create user cache")
|
logging.Log("EVENT-vJG2j").OnError(err).Panic("unable to create user cache")
|
||||||
|
|
||||||
return &UserCache{userCache: userCache}, nil
|
return &UserCache{userCache: userCache}, nil
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ func StartCache(conf *config.CacheConfig) (*UserCache, error) {
|
|||||||
func (c *UserCache) getUser(ID string) *model.User {
|
func (c *UserCache) getUser(ID string) *model.User {
|
||||||
user := &model.User{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
user := &model.User{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
||||||
if err := c.userCache.Get(ID, user); err != nil {
|
if err := c.userCache.Get(ID, user); err != nil {
|
||||||
logging.Log("EVENT-4eTZh").WithError(err).Debug("error in getting cache")
|
logging.Log("EVENT-AtS0S").WithError(err).Debug("error in getting cache")
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
@ -30,6 +30,6 @@ func (c *UserCache) getUser(ID string) *model.User {
|
|||||||
func (c *UserCache) cacheUser(user *model.User) {
|
func (c *UserCache) cacheUser(user *model.User) {
|
||||||
err := c.userCache.Set(user.AggregateID, user)
|
err := c.userCache.Set(user.AggregateID, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Log("EVENT-ThnBb").WithError(err).Debug("error in setting project cache")
|
logging.Log("EVENT-0V2gX").WithError(err).Debug("error in setting project cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ type UserGrantCache struct {
|
|||||||
|
|
||||||
func StartCache(conf *config.CacheConfig) (*UserGrantCache, error) {
|
func StartCache(conf *config.CacheConfig) (*UserGrantCache, error) {
|
||||||
userGrantCache, err := conf.Config.NewCache()
|
userGrantCache, err := conf.Config.NewCache()
|
||||||
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create user cache")
|
logging.Log("EVENT-8EhUZ").OnError(err).Panic("unable to create user grant cache")
|
||||||
|
|
||||||
return &UserGrantCache{userGrantCache: userGrantCache}, nil
|
return &UserGrantCache{userGrantCache: userGrantCache}, nil
|
||||||
}
|
}
|
||||||
@ -22,13 +22,13 @@ func StartCache(conf *config.CacheConfig) (*UserGrantCache, error) {
|
|||||||
func (c *UserGrantCache) getUserGrant(ID string) *model.UserGrant {
|
func (c *UserGrantCache) getUserGrant(ID string) *model.UserGrant {
|
||||||
user := &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
user := &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
|
||||||
err := c.userGrantCache.Get(ID, user)
|
err := c.userGrantCache.Get(ID, user)
|
||||||
logging.Log("EVENT-4eTZh").OnError(err).Debug("error in getting cache")
|
logging.Log("EVENT-QAd7T").OnError(err).Debug("error in getting cache")
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserGrantCache) cacheUserGrant(grant *model.UserGrant) {
|
func (c *UserGrantCache) cacheUserGrant(grant *model.UserGrant) {
|
||||||
err := c.userGrantCache.Set(grant.AggregateID, grant)
|
err := c.userGrantCache.Set(grant.AggregateID, grant)
|
||||||
|
|
||||||
logging.Log("EVENT-ThnBb").OnError(err).Debug("error in setting project cache")
|
logging.Log("EVENT-w2KNQ").OnError(err).Debug("error in setting user grant cache")
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eventsourcing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/caos/zitadel/internal/api/auth"
|
"github.com/caos/zitadel/internal/api/auth"
|
||||||
"github.com/caos/zitadel/internal/errors"
|
"github.com/caos/zitadel/internal/errors"
|
||||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||||
@ -209,10 +210,7 @@ func addUserGrantValidation(resourceOwner string, grant *model.UserGrant) func(.
|
|||||||
if !existsUser {
|
if !existsUser {
|
||||||
return errors.ThrowPreconditionFailed(nil, "EVENT-Sl8uS", "user doesn't exist")
|
return errors.ThrowPreconditionFailed(nil, "EVENT-Sl8uS", "user doesn't exist")
|
||||||
}
|
}
|
||||||
if err := checkProjectConditions(resourceOwner, grant, project); err != nil {
|
return checkProjectConditions(resourceOwner, grant, project)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,11 @@ var AuthService_AuthMethods = utils_auth.MethodMapping{
|
|||||||
CheckParam: "",
|
CheckParam: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"/caos.zitadel.auth.api.v1.AuthService/GetMyUserChanges": utils_auth.Option{
|
||||||
|
Permission: "authenticated",
|
||||||
|
CheckParam: "",
|
||||||
|
},
|
||||||
|
|
||||||
"/caos.zitadel.auth.api.v1.AuthService/UpdateMyUserAddress": utils_auth.Option{
|
"/caos.zitadel.auth.api.v1.AuthService/UpdateMyUserAddress": utils_auth.Option{
|
||||||
Permission: "authenticated",
|
Permission: "authenticated",
|
||||||
CheckParam: "",
|
CheckParam: "",
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -211,6 +211,45 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/me/changes": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "GetMyUserChanges",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A successful response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/v1Changes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"type": "string",
|
||||||
|
"format": "uint64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sequence_offset",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"type": "string",
|
||||||
|
"format": "uint64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "asc",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"type": "boolean",
|
||||||
|
"format": "boolean"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"AuthService"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/me/email": {
|
"/users/me/email": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "GetMyUserEmail",
|
"operationId": "GetMyUserEmail",
|
||||||
@ -552,7 +591,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "A successful response.",
|
"description": "A successful response.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/protobufStruct"
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -563,19 +602,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"protobufListValue": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"values": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/protobufValue"
|
|
||||||
},
|
|
||||||
"description": "Repeated field of dynamically typed values."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
|
|
||||||
},
|
|
||||||
"protobufNullValue": {
|
"protobufNullValue": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -584,50 +610,49 @@
|
|||||||
"default": "NULL_VALUE",
|
"default": "NULL_VALUE",
|
||||||
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
||||||
},
|
},
|
||||||
"protobufStruct": {
|
"v1Change": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"fields": {
|
"change_date": {
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"$ref": "#/definitions/protobufValue"
|
|
||||||
},
|
|
||||||
"description": "Unordered map of dynamically typed values."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
|
|
||||||
},
|
|
||||||
"protobufValue": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"null_value": {
|
|
||||||
"$ref": "#/definitions/protobufNullValue",
|
|
||||||
"description": "Represents a null value."
|
|
||||||
},
|
|
||||||
"number_value": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "double",
|
|
||||||
"description": "Represents a double value."
|
|
||||||
},
|
|
||||||
"string_value": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Represents a string value."
|
"format": "date-time"
|
||||||
},
|
},
|
||||||
"bool_value": {
|
"event_type": {
|
||||||
"type": "boolean",
|
"type": "string"
|
||||||
"format": "boolean",
|
|
||||||
"description": "Represents a boolean value."
|
|
||||||
},
|
},
|
||||||
"struct_value": {
|
"sequence": {
|
||||||
"$ref": "#/definitions/protobufStruct",
|
"type": "string",
|
||||||
"description": "Represents a structured value."
|
"format": "uint64"
|
||||||
},
|
},
|
||||||
"list_value": {
|
"editor_id": {
|
||||||
"$ref": "#/definitions/protobufListValue",
|
"type": "string"
|
||||||
"description": "Represents a repeated `Value`."
|
},
|
||||||
|
"editor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
|
},
|
||||||
|
"v1Changes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"changes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/v1Change"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uint64"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uint64"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"v1Gender": {
|
"v1Gender": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -128,3 +128,11 @@ func (s *Server) RemoveMfaOTP(ctx context.Context, _ *empty.Empty) (_ *empty.Emp
|
|||||||
s.repo.RemoveMyMfaOTP(ctx)
|
s.repo.RemoveMyMfaOTP(ctx)
|
||||||
return &empty.Empty{}, err
|
return &empty.Empty{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) GetMyUserChanges(ctx context.Context, request *ChangesRequest) (*Changes, error) {
|
||||||
|
changes, err := s.repo.MyUserChanges(ctx, request.SequenceOffset, request.Limit, request.Asc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return userChangesToResponse(changes, request.GetSequenceOffset(), request.GetLimit()), nil
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ package grpc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/caos/logging"
|
"github.com/caos/logging"
|
||||||
"github.com/caos/zitadel/internal/api/auth"
|
"github.com/caos/zitadel/internal/api/auth"
|
||||||
@ -9,6 +10,8 @@ import (
|
|||||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func userViewFromModel(user *usr_model.UserView) *UserView {
|
func userViewFromModel(user *usr_model.UserView) *UserView {
|
||||||
@ -335,3 +338,36 @@ func mfaTypeFromModel(mfatype usr_model.MfaType) MfaType {
|
|||||||
return MfaType_MFATYPE_UNSPECIFIED
|
return MfaType_MFATYPE_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userChangesToResponse(response *usr_model.UserChanges, offset uint64, limit uint64) (_ *Changes) {
|
||||||
|
return &Changes{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
Changes: userChangesToMgtAPI(response),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userChangesToMgtAPI(changes *usr_model.UserChanges) (_ []*Change) {
|
||||||
|
result := make([]*Change, len(changes.Changes))
|
||||||
|
|
||||||
|
for i, change := range changes.Changes {
|
||||||
|
var data *structpb.Struct
|
||||||
|
changedData, err := json.Marshal(change.Data)
|
||||||
|
if err == nil {
|
||||||
|
data = new(structpb.Struct)
|
||||||
|
err = protojson.Unmarshal(changedData, data)
|
||||||
|
logging.Log("GRPC-0kRsY").OnError(err).Debug("unable to marshal changed data to struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = &Change{
|
||||||
|
ChangeDate: change.ChangeDate,
|
||||||
|
EventType: change.EventType,
|
||||||
|
Sequence: change.Sequence,
|
||||||
|
Data: data,
|
||||||
|
EditorId: change.ModifierId,
|
||||||
|
Editor: change.ModifierName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -189,6 +189,16 @@ service AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpc GetMyUserChanges(ChangesRequest) returns (Changes) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/users/me/changes"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (caos.zitadel.utils.v1.auth_option) = {
|
||||||
|
permission: "authenticated"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
rpc UpdateMyUserAddress(UpdateUserAddressRequest) returns (UserAddress) {
|
rpc UpdateMyUserAddress(UpdateUserAddressRequest) returns (UserAddress) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
put: "/users/me/address"
|
put: "/users/me/address"
|
||||||
@ -623,3 +633,24 @@ enum SearchMethod {
|
|||||||
SEARCHMETHOD_STARTS_WITH_IGNORE_CASE = 4;
|
SEARCHMETHOD_STARTS_WITH_IGNORE_CASE = 4;
|
||||||
SEARCHMETHOD_CONTAINS_IGNORE_CASE = 5;
|
SEARCHMETHOD_CONTAINS_IGNORE_CASE = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ChangesRequest {
|
||||||
|
uint64 limit= 1;
|
||||||
|
uint64 sequence_offset = 2;
|
||||||
|
bool asc = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Changes {
|
||||||
|
repeated Change changes = 1;
|
||||||
|
uint64 offset = 2;
|
||||||
|
uint64 limit = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Change {
|
||||||
|
google.protobuf.Timestamp change_date = 1;
|
||||||
|
string event_type = 2;
|
||||||
|
uint64 sequence = 3;
|
||||||
|
string editor_id = 4;
|
||||||
|
string editor = 5;
|
||||||
|
google.protobuf.Struct data = 6;
|
||||||
|
}
|
@ -494,11 +494,14 @@ func userChangesToMgtAPI(changes *usr_model.UserChanges) (_ []*Change) {
|
|||||||
result := make([]*Change, len(changes.Changes))
|
result := make([]*Change, len(changes.Changes))
|
||||||
|
|
||||||
for i, change := range changes.Changes {
|
for i, change := range changes.Changes {
|
||||||
b, err := json.Marshal(change.Data)
|
var data *structpb.Struct
|
||||||
data := &structpb.Struct{}
|
changedData, err := json.Marshal(change.Data)
|
||||||
err = protojson.Unmarshal(b, data)
|
if err == nil {
|
||||||
if err != nil {
|
data = new(structpb.Struct)
|
||||||
|
err = protojson.Unmarshal(changedData, data)
|
||||||
|
logging.Log("GRPC-a7F54").OnError(err).Debug("unable to marshal changed data to struct")
|
||||||
}
|
}
|
||||||
|
|
||||||
result[i] = &Change{
|
result[i] = &Change{
|
||||||
ChangeDate: change.ChangeDate,
|
ChangeDate: change.ChangeDate,
|
||||||
EventType: change.EventType,
|
EventType: change.EventType,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user