mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-05 05:42:16 +00:00
feat: set metadata field in UpdateUser endpoint (#11747)
# Which Problems Are Solved To be able to update metadata via the `UpdateUser` endpoint. # How the Problems Are Solved This is achieved by: * adding the `Metadata` field to `UpdateUserRequest` * update the server layer to convert/set metadata fields to domain layer metadata * update the command layer to handle metadata updates # Additional Changes N/A # Additional Context - Related to https://github.com/zitadel/zitadel/issues/11369, https://github.com/zitadel/zitadel/issues/11759 - Follow-up for PR https://github.com/zitadel/zitadel/pull/11719 --------- Co-authored-by: Copilot <[email protected]>
This commit is contained in:
co-authored by
Copilot
parent
5b12690757
commit
282b2486d9
@@ -67,36 +67,14 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne
|
||||
}
|
||||
}
|
||||
|
||||
events := make([]eventstore.Command, 0)
|
||||
setMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel)
|
||||
for _, data := range metadatas {
|
||||
existingValue, keyExists := setMetadata.metadataList[data.Key]
|
||||
|
||||
// if value is empty, a metadata remove event has to be pushed
|
||||
if len(data.Value) == 0 {
|
||||
// Ignore deletion if key does not exist
|
||||
if !keyExists {
|
||||
continue
|
||||
}
|
||||
|
||||
event := user.NewMetadataRemovedEvent(ctx, userAgg, data.Key)
|
||||
events = append(events, event)
|
||||
continue
|
||||
}
|
||||
|
||||
// if no change to metadata no event has to be pushed
|
||||
if keyExists && bytes.Equal(existingValue, data.Value) {
|
||||
continue
|
||||
}
|
||||
event, err := c.setUserMetadata(ctx, userAgg, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, event)
|
||||
events, err := c.createMetadataEvents(ctx, metadatas, setMetadata.metadataList, userAgg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// no changes for the metadata
|
||||
if len(events) == 0 {
|
||||
@@ -115,6 +93,37 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne
|
||||
return writeModelToObjectDetails(&setMetadata.WriteModel), nil
|
||||
}
|
||||
|
||||
// createMetadataEvents compares the existing metadata key-value pair before creating metadata set/remove events
|
||||
func (c *Commands) createMetadataEvents(ctx context.Context, reqMetadata []*domain.Metadata, currentMetadataMap map[string][]byte, userAgg *eventstore.Aggregate) ([]eventstore.Command, error) {
|
||||
events := make([]eventstore.Command, 0)
|
||||
for _, data := range reqMetadata {
|
||||
existingValue, keyExists := currentMetadataMap[data.Key]
|
||||
|
||||
// if the value is empty, a metadata remove event has to be pushed
|
||||
if len(data.Value) == 0 {
|
||||
// Ignore deletion if the key does not exist
|
||||
if !keyExists {
|
||||
continue
|
||||
}
|
||||
|
||||
event := user.NewMetadataRemovedEvent(ctx, userAgg, data.Key)
|
||||
events = append(events, event)
|
||||
continue
|
||||
}
|
||||
|
||||
// if no change to metadata, no event has to be pushed
|
||||
if keyExists && bytes.Equal(existingValue, data.Value) {
|
||||
continue
|
||||
}
|
||||
event, err := c.setUserMetadata(ctx, userAgg, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (c *Commands) setUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadata *domain.Metadata) (command eventstore.Command, err error) {
|
||||
if !metadata.IsValid() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "META-2m00f", "Errors.Metadata.Invalid")
|
||||
|
||||
@@ -704,6 +704,121 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add and delete metadata, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"",
|
||||
"firstname lastname",
|
||||
language.Und,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewMetadataSetEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"key1",
|
||||
[]byte("value1"),
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewMetadataSetEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"key2",
|
||||
[]byte("value2"),
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
user.NewMetadataSetEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"key1",
|
||||
[]byte("updated_value1"),
|
||||
),
|
||||
user.NewMetadataRemovedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"key2",
|
||||
),
|
||||
user.NewMetadataSetEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"key3",
|
||||
[]byte("value3"),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
userID: "user1",
|
||||
metadataList: []*domain.Metadata{
|
||||
{Key: "key1", Value: []byte("updated_value1")},
|
||||
{Key: "key2"},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete non existing key, ok (ignored)",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"",
|
||||
"firstname lastname",
|
||||
language.Und,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewMetadataSetEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"key1",
|
||||
[]byte("value1"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
userID: "user1",
|
||||
metadataList: []*domain.Metadata{
|
||||
{Key: "key2"},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -337,13 +337,12 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg
|
||||
}
|
||||
}
|
||||
|
||||
for _, md := range human.Metadata {
|
||||
cmd, err := c.setUserMetadata(ctx, userAgg, md)
|
||||
if len(human.Metadata) > 0 {
|
||||
metadataCmds, err := c.createMetadataEvents(ctx, human.Metadata, existingHuman.Metadata, userAgg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
cmds = append(cmds, metadataCmds...)
|
||||
}
|
||||
|
||||
for _, mdKey := range human.MetadataKeysToRemove {
|
||||
|
||||
@@ -3919,6 +3919,148 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change human metadata, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("$plain$x$password", true, true, "", language.English),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key1",
|
||||
[]byte("value1"),
|
||||
),
|
||||
user.NewMetadataSetEvent(context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key3",
|
||||
[]byte("value3"),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
loginPaths: expectLoginPathsNoCall,
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &ChangeHuman{
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
{
|
||||
Key: "key3",
|
||||
Value: []byte("value3"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
Sequence: 0,
|
||||
EventDate: time.Time{},
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change human metadata, empty value, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("$plain$x$password", true, true, "", language.English),
|
||||
),
|
||||
eventFromEventPusher( // pre-existing metadata
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key1",
|
||||
[]byte("value1")),
|
||||
),
|
||||
eventFromEventPusher( // pre-existing metadata
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key2",
|
||||
[]byte("value2")),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
user.NewMetadataRemovedEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key2",
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
loginPaths: expectLoginPathsNoCall,
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &ChangeHuman{
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "key1",
|
||||
Value: []byte("value1"), // no update
|
||||
},
|
||||
{
|
||||
Key: "key2",
|
||||
Value: []byte(""), // delete the key for an empty value
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
Sequence: 0,
|
||||
EventDate: time.Time{},
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change human metadata, no permission",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("$plain$x$password", true, true, "", language.English),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
tarpit: expectTarpit(0),
|
||||
loginPaths: expectLoginPathsNoCall,
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &ChangeHuman{
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -20,6 +20,8 @@ type ChangeMachine struct {
|
||||
|
||||
// Details are set after a successful execution of the command
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
Metadata []*domain.Metadata
|
||||
}
|
||||
|
||||
func (h *ChangeMachine) Changed() bool {
|
||||
@@ -35,25 +37,35 @@ func (h *ChangeMachine) Changed() bool {
|
||||
if h.AccessTokenType != nil {
|
||||
return true
|
||||
}
|
||||
if len(h.Metadata) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine) (err error) {
|
||||
// get the existing user
|
||||
existingMachine, err := c.UserMachineWriteModel(
|
||||
ctx,
|
||||
machine.ID,
|
||||
machine.ResourceOwner,
|
||||
false,
|
||||
len(machine.Metadata) > 0,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if machine.Changed() {
|
||||
if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
// check whether the user has permissions to make this change
|
||||
if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if there are no changes, return the existing object details
|
||||
if !machine.Changed() {
|
||||
machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel)
|
||||
return nil
|
||||
}
|
||||
|
||||
// create the events for the change
|
||||
cmds := make([]eventstore.Command, 0)
|
||||
if machine.Username != nil {
|
||||
cmds, err = c.changeUsername(ctx, cmds, existingMachine, *machine.Username)
|
||||
@@ -74,6 +86,14 @@ func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine
|
||||
if len(machineChanges) > 0 {
|
||||
cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges))
|
||||
}
|
||||
if len(machine.Metadata) > 0 {
|
||||
metadataCmds, err := c.createMetadataEvents(ctx, machine.Metadata, existingMachine.Metadata, &existingMachine.Aggregate().Aggregate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmds = append(cmds, metadataCmds...)
|
||||
}
|
||||
|
||||
if len(cmds) == 0 {
|
||||
machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel)
|
||||
return nil
|
||||
|
||||
@@ -338,6 +338,133 @@ func TestCommandSide_ChangeUserMachine(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change machine metadata, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(userAddedEvent),
|
||||
),
|
||||
expectPush(
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key1",
|
||||
[]byte("value1"),
|
||||
),
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key3",
|
||||
[]byte("value3"),
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
machine: &ChangeMachine{
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
{
|
||||
Key: "key3",
|
||||
Value: []byte("value3"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change machine metadata, delete metadata, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(userAddedEvent),
|
||||
eventFromEventPusher( // pre-existing metadata
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key1",
|
||||
[]byte("value1")),
|
||||
),
|
||||
eventFromEventPusher( // pre-existing metadata
|
||||
user.NewMetadataSetEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key2",
|
||||
[]byte("value2")),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
user.NewMetadataRemovedEvent(
|
||||
context.Background(),
|
||||
&userAgg.Aggregate,
|
||||
"key2",
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
machine: &ChangeMachine{
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
{
|
||||
Key: "key2",
|
||||
Value: []byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change machine metadata, no permission",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(userAddedEvent),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
machine: &ChangeMachine{
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "key1",
|
||||
Value: []byte("value1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user