mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-10 16:15:12 +00:00
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:
co-authored by
Gayathri Vijayan
Copilot
parent
3443507c20
commit
247e278505
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user