feat: delete metadata on SetUserMetadata api (#11776)

# Which Problems Are Solved

Currently, to delete metadata a key, callers must use a separate
[DeleteMetadata](https://zitadel.com/docs/reference/api/user/zitadel.user.v2.UserService.DeleteUserMetadata)
API. This increases the complexity on client-side for operations where
metadata need to be synchronized.

# How the Problems Are Solved

Introduce the behavior to
[SetUserMetadata](https://zitadel.com/docs/reference/api/user/zitadel.user.v2.UserService.SetUserMetadata):

When a metadata entry is passed with an empty value ("" / empty bytes):

- If the key exists → delete the key
- If the key does not exist → no-op (idempotent, no error)

# Additional Changes

- Updated [User Metadata
page](https://zitadel.com/docs/guides/manage/customize/user-metadata#manage-user-metadata-through-the-management-api)

# Additional Context

Similar behavior will be applied to
[UpdateUser](https://zitadel.com/docs/reference/api/user/zitadel.user.v2.UserService.UpdateUser)
and
[SetOrganizationMetadata](https://zitadel.com/docs/reference/api/org/zitadel.org.v2.OrganizationService.SetOrganizationMetadata)

Breaking change? 
Currently, sending an empty value returns an error — no client should be
intentionally relying on that error as part of a working workflow. The
change goes from "rejected input" to "accepted input with defined
semantics."

In other words, it relaxes a restriction rather than tightening one.
Existing valid calls continue to work exactly as before. The only
scenario where it could be "breaking" is if someone explicitly depends
on the error response for empty values (e.g., using it as a validation
check), which would be unusual.

---------

Co-authored-by: Gayathri Vijayan <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Vitor Bari Buccianti
2026-03-06 14:57:52 +01:00
committed by GitHub
co-authored by Gayathri Vijayan Copilot
parent 3443507c20
commit 247e278505
6 changed files with 205 additions and 69 deletions
@@ -34,7 +34,7 @@ You will find more information in our guides on how to [authenticate users](/gui
Use one of these methods to get the metadata for the currently logged in user.
In case you want to manage metadata for other users than the currently logged in user, then you must use the [management service](#manage-user-metadata-through-the-management-api).
In case you want to manage metadata for other users than the currently logged in user, then you must use the [UserService API](#manage-user-metadata-through-the-api).
### Request metadata from userinfo endpoint
@@ -217,12 +217,17 @@ An example response for your search looks like this:
When you build your own registration UI you have the possibility to have custom fields and add them to the metadata of your user.
Learn everything about how to build your own registration UI [here](/guides/integrate/onboarding/end-users#build-your-own-registration-form).
## Manage user metadata through the management API
## Manage user metadata through the API
The previous methods allowed you to retrieve metadata only for the `sub` in the access token.
In case you want to get the metadata for another user, you need to use the management service.
The user that calls the management service must have [administrator permissions](/guides/manage/console/administrators).
A user can be either a User (Human) or a Service Account.
In case you want to manage metadata for another user, use the [User Service API](/reference/api/user).
The calling user must have the `user.write` permission to set or delete metadata.
You can get [metadata of a user filtered by your query](/apis/resources/mgmt/management-service-list-user-metadata) or [get a metadata object from a user by a specific key](/apis/resources/mgmt/management-service-get-user-metadata).
The management service allows you to set and delete metadata, see the [API documentation for users](/reference/api/user).
Use [SetUserMetadata](/apis/resources/user_service_v2/user-service-set-user-metadata) to insert or update metadata entries.
Existing keys not included in the request are left unchanged.
To delete a metadata entry, pass the key with an empty value — if the key does not exist, it is ignored.
You can mix inserts, updates, and deletions in a single request.
Alternatively, use [DeleteUserMetadata](/apis/resources/user_service_v2/user-service-delete-user-metadata) to delete one or more keys explicitly.
Use [ListUserMetadata](/apis/resources/user_service_v2/user-service-list-user-metadata) to retrieve metadata for a user with optional filtering by key.
@@ -25,7 +25,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
ctx context.Context
dep func(request *user.SetUserMetadataRequest)
req *user.SetUserMetadataRequest
setDate bool
assert func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse)
wantErr bool
}{
{
@@ -48,7 +48,14 @@ func TestServer_SetUserMetadata(t *testing.T) {
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}},
},
setDate: true,
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
assert.WithinRange(t, setResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
}
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
},
},
{
name: "set user metadata, multiple",
@@ -63,7 +70,16 @@ func TestServer_SetUserMetadata(t *testing.T) {
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
},
},
setDate: true,
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
assert.WithinRange(t, setResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
}
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
},
},
{
name: "set user metadata on non existent user",
@@ -84,7 +100,14 @@ func TestServer_SetUserMetadata(t *testing.T) {
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}},
},
setDate: true,
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
assert.WithinRange(t, setResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
}
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
},
},
{
name: "update user metadata with same value",
@@ -96,37 +119,100 @@ func TestServer_SetUserMetadata(t *testing.T) {
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}},
},
setDate: true,
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
// assert that the set date is not updated, i.e., it is before testStartTime, because no change to the metadata should be made
assert.True(t, setResponse.GetSetDate().AsTime().Before(testStartTime))
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
}
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
},
},
{
name: "delete user metadata",
ctx: Instance.WithAuthorizationToken(OrgCTX, integration.UserTypeIAMOwner),
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1")
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{{Key: "key1", Value: nil}},
},
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
assert.WithinRange(t, setResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
assert.Len(t, getResponse.GetMetadata(), 0)
},
},
{
name: "delete non-existent user metadata key is noop",
ctx: Instance.WithAuthorizationToken(OrgCTX, integration.UserTypeIAMOwner),
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1")
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{
{Key: "key2", Value: nil}, // delete non-existent key; should be a no-op
},
},
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
// assert that the set date is not updated, i.e., it is before testStartTime, because no change to the metadata should be made
assert.True(t, setResponse.GetSetDate().AsTime().Before(testStartTime))
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
}
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
},
},
{
name: "update and delete user metadata",
ctx: Instance.WithAuthorizationToken(OrgCTX, integration.UserTypeIAMOwner),
dep: func(req *user.SetUserMetadataRequest) {
req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1")
Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key2", "value2")
},
req: &user.SetUserMetadataRequest{
Metadata: []*user.Metadata{
{Key: "key1", Value: nil}, // delete
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated")))},
},
},
assert: func(t *testing.T, setResponse *user.SetUserMetadataResponse, testStartTime time.Time, getResponse *user.ListUserMetadataResponse) {
assert.WithinRange(t, setResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
expectedMetadata := []*metadata.Metadata{
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated")))},
}
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
if tt.dep != nil {
tt.dep(tt.req)
}
testStartTime := time.Now().UTC()
got, err := Client.SetUserMetadata(tt.ctx, tt.req)
changeDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetUserMetadataResponse(t, creationDate, changeDate, tt.setDate, got)
})
}
}
Instance.TriggerUserByID(tt.ctx, tt.req.GetUserId())
func assertSetUserMetadataResponse(t *testing.T, creationDate, changeDate time.Time, expectedSetDat bool, actualResp *user.SetUserMetadataResponse) {
if expectedSetDat {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.SetDate)
gotMetadataResponse, err := Client.ListUserMetadata(tt.ctx, &user.ListUserMetadataRequest{
UserId: tt.req.GetUserId(),
})
require.NoError(t, err)
tt.assert(t, got, testStartTime, gotMetadataResponse)
})
}
}
@@ -445,13 +531,13 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
name: "delete",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
testStartTime := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
request.UserId = userID
key := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1")
request.Keys = []string{key}
return creationDate, time.Time{}
return testStartTime, time.Time{}
},
req: &user.DeleteUserMetadataRequest{},
wantDeletionDate: true,
@@ -460,13 +546,13 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
name: "delete, empty list",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
testStartTime := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
request.UserId = userID
key := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1")
Instance.DeleteUserMetadata(iamOwnerCTX, userID, key)
return creationDate, time.Now().UTC()
return testStartTime, time.Now().UTC()
},
req: &user.DeleteUserMetadataRequest{},
wantErr: true,
@@ -475,14 +561,14 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
name: "delete, already removed",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
testStartTime := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
request.UserId = userID
key := "key1"
Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1")
Instance.DeleteUserMetadata(iamOwnerCTX, userID, key)
request.Keys = []string{key}
return creationDate, time.Now().UTC()
return testStartTime, time.Now().UTC()
},
req: &user.DeleteUserMetadataRequest{},
wantErr: true,
@@ -491,7 +577,7 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
name: "delete, multiple",
ctx: iamOwnerCTX,
prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
testStartTime := time.Now().UTC()
userID := Instance.CreateUserTypeHuman(iamOwnerCTX, integration.Email()).GetId()
request.UserId = userID
key1 := "key1"
@@ -501,7 +587,7 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
key3 := "key3"
Instance.SetUserMetadata(iamOwnerCTX, userID, key3, "value1")
request.Keys = []string{key1, key2, key3}
return creationDate, time.Time{}
return testStartTime, time.Time{}
},
req: &user.DeleteUserMetadataRequest{},
wantDeletionDate: true,
@@ -509,9 +595,9 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var creationDate, deletionDate time.Time
var testStartTime, deletionDate time.Time
if tt.prepare != nil {
creationDate, deletionDate = tt.prepare(tt.req)
testStartTime, deletionDate = tt.prepare(tt.req)
}
got, err := Instance.Client.UserV2.DeleteUserMetadata(tt.ctx, tt.req)
if tt.wantErr {
@@ -519,17 +605,17 @@ func TestServer_DeleteUserMetadata(t *testing.T) {
return
}
assert.NoError(t, err)
assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got)
assertDeleteProjectResponse(t, testStartTime, deletionDate, tt.wantDeletionDate, got)
})
}
}
func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *user.DeleteUserMetadataResponse) {
func assertDeleteProjectResponse(t *testing.T, testStartTime, deletionDate time.Time, expectedDeletionDate bool, actualResp *user.DeleteUserMetadataResponse) {
if expectedDeletionDate {
if !deletionDate.IsZero() {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate)
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), testStartTime, deletionDate)
} else {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC())
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), testStartTime, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.DeletionDate)
@@ -21,6 +21,7 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/idp"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
user_v1 "github.com/zitadel/zitadel/pkg/grpc/user"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
@@ -3585,10 +3586,11 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
},
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, getMetadataResponse *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
metadataByKey := getMetadataMap(getMetadataResponse)
assert.Len(t, getMetadataResponse.GetMetadata(), 2)
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), metadataByKey["key1"])
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value2"))), metadataByKey["key2"])
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
}
assertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
},
}
},
@@ -3622,9 +3624,10 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
},
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, getMetadataResponse *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
metadataByKey := getMetadataMap(getMetadataResponse)
assert.Len(t, getMetadataResponse.GetMetadata(), 1)
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), metadataByKey["key1"])
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
}
assertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
},
}
},
@@ -3704,10 +3707,12 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
},
assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse, getMetadataResponse *user.ListUserMetadataResponse) {
assert.Equal(t, username, getResponse.GetUser().GetUsername())
metadataByKey := getMetadataMap(getMetadataResponse)
assert.Len(t, getMetadataResponse.GetMetadata(), 2)
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), metadataByKey["key1"])
assert.Equal(t, []byte(base64.StdEncoding.EncodeToString([]byte("value2"))), metadataByKey["key2"])
expectedMetadata := []*metadata.Metadata{
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
}
assertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
},
}
},
@@ -3800,14 +3805,19 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
}
}
func getMetadataMap(getMetadataResponse *user.ListUserMetadataResponse) map[string][]byte {
metadataByKey := make(map[string][]byte, len(getMetadataResponse.GetMetadata()))
for _, md := range getMetadataResponse.GetMetadata() {
func getMetadataMap(metadata []*metadata.Metadata) map[string][]byte {
metadataByKey := make(map[string][]byte, len(metadata))
for _, md := range metadata {
metadataByKey[md.Key] = md.Value
}
return metadataByKey
}
func assertMetadataEquals(t *testing.T, expected []*metadata.Metadata, actual []*metadata.Metadata) {
assert.Equal(t, len(expected), len(actual))
assert.Equal(t, getMetadataMap(expected), getMetadataMap(actual))
}
func TestServer_CreateUser_Permission(t *testing.T) {
newOrgOwnerEmail := integration.Email()
newOrg := Instance.CreateOrganization(IamCTX, integration.OrganizationName(), newOrgOwnerEmail)
+15 -1
View File
@@ -74,8 +74,22 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne
}
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 existingValue, ok := setMetadata.metadataList[data.Key]; ok && bytes.Equal(existingValue, data.Value) {
if keyExists && bytes.Equal(existingValue, data.Value) {
continue
}
event, err := c.setUserMetadata(ctx, userAgg, data)
+31 -10
View File
@@ -491,7 +491,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
},
},
{
name: "invalid metadata, pre condition error",
name: "empty metadata value, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
@@ -523,7 +523,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
},
},
res: res{
err: zerrors.IsErrorInvalidArgument,
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
@@ -584,17 +586,34 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
),
),
),
expectFilter(),
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.NewMetadataRemovedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
[]byte("value"),
"key1",
),
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key1",
[]byte("value1"),
"key2",
[]byte("update"),
),
user.NewMetadataSetEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key3",
[]byte("add"),
),
),
),
@@ -604,8 +623,10 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
orgID: "org1",
userID: "user1",
metadataList: []*domain.Metadata{
{Key: "key", Value: []byte("value")},
{Key: "key1", Value: []byte("value1")},
{Key: "key1"}, // delete
{Key: "key2", Value: []byte("update")},
{Key: "key3", Value: []byte("add")},
{Key: "key4"}, // ignore
},
},
res: res{
+3 -3
View File
@@ -1912,7 +1912,7 @@ service UserService {
}
// Set User Metadata
//
// Sets a list of key value pairs. Existing metadata entries with matching keys are overwritten. Existing metadata entries without matching keys are untouched. To remove metadata entries, use [DeleteUserMetadata](/docs/reference/api/user/zitadel.user.v2.UserService.DeleteUserMetadata). For HTTP requests, make sure the bytes array value is base64 encoded.
// Sets a list of key value pairs. Existing metadata entries with matching keys are overwritten. Existing metadata entries without matching keys are untouched. To remove existing metadata entries, pass an empty metadata value or use [DeleteUserMetadata](/docs/reference/api/user/zitadel.user.v2.UserService.DeleteUserMetadata). If no metadata entry exists for a given key, passing an empty value for that key has no effect (no-op). For HTTP requests, make sure the bytes array value is base64 encoded.
//
// Required permission:
// - `user.write`
@@ -3736,8 +3736,8 @@ message ListPersonalAccessTokensResponse {
message Metadata {
// Key in the metadata key/value pair.
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
// Value in the metadata key/value pair.
bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
// Value in the metadata key/value pair. On updates, an empty value will delete the metadata entry.
bytes value = 2 [(validate.rules).bytes = {max_len: 500000}];
}
message SetUserMetadataRequest{