mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat: delete metadata on SetOrganizationMetadata api (#11790)
# Which Problems Are Solved Currently, to delete an Organization metadata key, callers must use a separate [DeleteMetadata](https://zitadel.com/docs/reference/api/org/zitadel.org.v2.OrganizationService.DeleteOrganizationMetadata) API. This increases the complexity on client-side for operations where metadata need to be synchronized. # How the Problems Are Solved Introduce deletion behavior to [SetOrganizationMetadata](https://zitadel.com/docs/reference/api/org/zitadel.org.v2.OrganizationService.SetOrganizationMetadata): 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 n/a # Additional Context Relates to #11776 --------- Co-authored-by: Gayathri Vijayan <[email protected]> Co-authored-by: Gayathri Vijayan <[email protected]>
This commit is contained in:
co-authored by
Gayathri Vijayan
Gayathri Vijayan
parent
8b90ddb0b5
commit
62cb5bbb83
@@ -4,6 +4,7 @@ package org_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"os"
|
||||
"slices"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
@@ -280,7 +282,7 @@ func TestServer_UpdateOrganization(t *testing.T) {
|
||||
name: "update org with non existent org id",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
req: &org.UpdateOrganizationRequest{
|
||||
OrganizationId: "non existant org id",
|
||||
OrganizationId: "non existent org id",
|
||||
// Name: "",
|
||||
},
|
||||
wantErr: true,
|
||||
@@ -1029,125 +1031,159 @@ func TestServer_ValidateOrganizationDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_SetOrganizationMetadata(t *testing.T) {
|
||||
orgs, _, _ := createOrgs(CTX, t, Client, 1)
|
||||
orgId := orgs[0].OrganizationId
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
setupFunc func()
|
||||
orgId string
|
||||
key string
|
||||
value string
|
||||
setupFunc func(ctx context.Context, orgID string, req *org.SetOrganizationMetadataRequest)
|
||||
req *org.SetOrganizationMetadataRequest
|
||||
assert func(ttt *assert.CollectT, setMetadataResponse *org.SetOrganizationMetadataResponse, testStartTime time.Time, listMetadataResponse *org.ListOrganizationMetadataResponse)
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "no permission",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
|
||||
orgId: orgId,
|
||||
key: "key1",
|
||||
value: "value1",
|
||||
setupFunc: func(ctx context.Context, orgID string, req *org.SetOrganizationMetadataRequest) {
|
||||
req.OrganizationId = orgID
|
||||
},
|
||||
req: &org.SetOrganizationMetadataRequest{
|
||||
Metadata: []*org.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
},
|
||||
},
|
||||
err: errors.New("membership not found"),
|
||||
},
|
||||
{
|
||||
name: "set org metadata",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
orgId: orgId,
|
||||
key: "key1",
|
||||
value: "value1",
|
||||
setupFunc: func(ctx context.Context, orgID string, req *org.SetOrganizationMetadataRequest) {
|
||||
req.OrganizationId = orgID
|
||||
},
|
||||
req: &org.SetOrganizationMetadataRequest{
|
||||
Metadata: []*org.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
},
|
||||
},
|
||||
assert: func(ttt *assert.CollectT, setMetadataResponse *org.SetOrganizationMetadataResponse, testStartTime time.Time, listMetadataResponse *org.ListOrganizationMetadataResponse) {
|
||||
assert.WithinRange(ttt, setMetadataResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
|
||||
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
}
|
||||
integration.AssertMetadataEquals(ttt, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set org metadata on non existant org",
|
||||
name: "set org metadata on non existent org",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
orgId: "non existant orgid",
|
||||
key: "key2",
|
||||
value: "value2",
|
||||
setupFunc: func(ctx context.Context, _ string, req *org.SetOrganizationMetadataRequest) {
|
||||
req.OrganizationId = "wrong-org-id"
|
||||
},
|
||||
req: &org.SetOrganizationMetadataRequest{
|
||||
Metadata: []*org.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated-value1")))},
|
||||
},
|
||||
},
|
||||
err: errors.New("Organisation not found"),
|
||||
},
|
||||
{
|
||||
name: "update org metadata",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
setupFunc: func() {
|
||||
_, err := Client.SetOrganizationMetadata(CTX, &org.SetOrganizationMetadataRequest{
|
||||
OrganizationId: orgId,
|
||||
setupFunc: func(ctx context.Context, orgID string, req *org.SetOrganizationMetadataRequest) {
|
||||
req.OrganizationId = orgID
|
||||
Instance.SetOrganizationMetadata(ctx, orgID, "key1", "value1")
|
||||
},
|
||||
req: &org.SetOrganizationMetadataRequest{
|
||||
Metadata: []*org.Metadata{
|
||||
{
|
||||
Key: "key3",
|
||||
Value: []byte("value3"),
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated-value1")))},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert: func(ttt *assert.CollectT, setMetadataResponse *org.SetOrganizationMetadataResponse, testStartTime time.Time, listMetadataResponse *org.ListOrganizationMetadataResponse) {
|
||||
assert.WithinRange(ttt, setMetadataResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
|
||||
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated-value1")))},
|
||||
}
|
||||
integration.AssertMetadataEquals(ttt, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
orgId: orgId,
|
||||
key: "key4",
|
||||
value: "value4",
|
||||
},
|
||||
{
|
||||
name: "update org metadata with same value",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
setupFunc: func() {
|
||||
_, err := Client.SetOrganizationMetadata(CTX, &org.SetOrganizationMetadataRequest{
|
||||
OrganizationId: orgId,
|
||||
setupFunc: func(ctx context.Context, orgID string, req *org.SetOrganizationMetadataRequest) {
|
||||
req.OrganizationId = orgID
|
||||
Instance.SetOrganizationMetadata(ctx, orgID, "key1", "value1")
|
||||
},
|
||||
req: &org.SetOrganizationMetadataRequest{
|
||||
Metadata: []*org.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
},
|
||||
},
|
||||
assert: func(ttt *assert.CollectT, setMetadataResponse *org.SetOrganizationMetadataResponse, testStartTime time.Time, listMetadataResponse *org.ListOrganizationMetadataResponse) {
|
||||
assert.True(ttt, setMetadataResponse.GetSetDate().AsTime().Before(testStartTime))
|
||||
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
}
|
||||
integration.AssertMetadataEquals(ttt, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "key5",
|
||||
Value: []byte("value5"),
|
||||
name: "delete and update org metadata",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
setupFunc: func(ctx context.Context, orgID string, req *org.SetOrganizationMetadataRequest) {
|
||||
req.OrganizationId = orgID
|
||||
Instance.SetOrganizationMetadata(ctx, orgID, "key1", "value1")
|
||||
Instance.SetOrganizationMetadata(ctx, orgID, "key2", "value2")
|
||||
},
|
||||
req: &org.SetOrganizationMetadataRequest{
|
||||
Metadata: []*org.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated-value1")))}, // updating
|
||||
{Key: "key2"}, // deleting
|
||||
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))}, // adding
|
||||
{Key: "key4"}, // deleting non-existent
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert: func(ttt *assert.CollectT, setMetadataResponse *org.SetOrganizationMetadataResponse, testStartTime time.Time, listMetadataResponse *org.ListOrganizationMetadataResponse) {
|
||||
assert.WithinRange(ttt, setMetadataResponse.GetSetDate().AsTime(), testStartTime, time.Now().UTC())
|
||||
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated-value1")))},
|
||||
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
|
||||
}
|
||||
integration.AssertMetadataEquals(ttt, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
orgId: orgId,
|
||||
key: "key5",
|
||||
value: "value5",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orgResponse := Instance.CreateOrganization(
|
||||
CTX,
|
||||
integration.OrganizationName(),
|
||||
integration.Email(),
|
||||
)
|
||||
|
||||
if tt.setupFunc != nil {
|
||||
tt.setupFunc()
|
||||
tt.setupFunc(tt.ctx, orgResponse.OrganizationId, tt.req)
|
||||
}
|
||||
got, err := Client.SetOrganizationMetadata(tt.ctx, &org.SetOrganizationMetadataRequest{
|
||||
OrganizationId: tt.orgId,
|
||||
Metadata: []*org.Metadata{
|
||||
{
|
||||
Key: tt.key,
|
||||
Value: []byte(tt.value),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
testStartTime := time.Now().UTC()
|
||||
got, err := Client.SetOrganizationMetadata(tt.ctx, tt.req)
|
||||
if tt.err != nil {
|
||||
require.Contains(t, err.Error(), tt.err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// check details
|
||||
gotCD := got.GetSetDate().AsTime()
|
||||
now := time.Now()
|
||||
assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute))
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute)
|
||||
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
// check metadata
|
||||
listMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &org.ListOrganizationMetadataRequest{
|
||||
OrganizationId: orgId,
|
||||
OrganizationId: orgResponse.OrganizationId,
|
||||
})
|
||||
require.NoError(ttt, err)
|
||||
foundMetadata := false
|
||||
foundMetadataKeyCount := 0
|
||||
for _, res := range listMetadataRes.Metadata {
|
||||
if res.Key == tt.key {
|
||||
foundMetadataKeyCount += 1
|
||||
}
|
||||
if res.Key == tt.key &&
|
||||
string(res.Value) == tt.value {
|
||||
foundMetadata = true
|
||||
}
|
||||
}
|
||||
require.True(ttt, foundMetadata, "unable to find added metadata")
|
||||
require.Equal(ttt, 1, foundMetadataKeyCount, "same metadata key found multiple times")
|
||||
tt.assert(ttt, got, testStartTime, listMetadataRes)
|
||||
}, retryDuration, tick, "timeout waiting for expected organizations being created")
|
||||
})
|
||||
}
|
||||
@@ -1242,7 +1278,7 @@ func TestServer_DeleteOrganizationMetadata(t *testing.T) {
|
||||
{
|
||||
name: "delete org metadata for org that does not exist",
|
||||
ctx: Instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner),
|
||||
orgId: "non existant org id",
|
||||
orgId: "non existent org id",
|
||||
metadataToDelete: []struct{ key, value string }{
|
||||
{
|
||||
key: "key4",
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -78,7 +78,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
|
||||
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
|
||||
{Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -126,7 +126,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -164,7 +164,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -187,7 +187,7 @@ func TestServer_SetUserMetadata(t *testing.T) {
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("updated")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getResponse.GetMetadata())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3590,7 +3590,7 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -3627,7 +3627,7 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
|
||||
expectedMetadata := []*metadata.Metadata{
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -3712,7 +3712,7 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
|
||||
{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))},
|
||||
{Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, getMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -3805,19 +3805,6 @@ func TestServer_CreateUser_And_Compare(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -4615,7 +4602,7 @@ func TestServer_UpdateUser_And_Compare(t *testing.T) {
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -4683,7 +4670,7 @@ func TestServer_UpdateUser_And_Compare(t *testing.T) {
|
||||
{Key: "key1", Value: []byte("updated_value1")},
|
||||
{Key: "key4", Value: []byte("value4")},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -4740,7 +4727,7 @@ func TestServer_UpdateUser_And_Compare(t *testing.T) {
|
||||
{Key: "key2", Value: []byte("value2")},
|
||||
{Key: "key3", Value: []byte("value3")},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -4802,7 +4789,7 @@ func TestServer_UpdateUser_And_Compare(t *testing.T) {
|
||||
{Key: "key1", Value: []byte("updated_value1")},
|
||||
{Key: "key4", Value: []byte("value4")},
|
||||
}
|
||||
assertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
integration.AssertMetadataEquals(t, expectedMetadata, listMetadataResponse.GetMetadata())
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -46,15 +47,42 @@ func (c *Commands) BulkSetOrgMetadata(ctx context.Context, orgID string, permiss
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := make([]eventstore.Command, len(metadatas))
|
||||
setMetadata := NewOrgMetadataListWriteModel(orgID)
|
||||
events := make([]eventstore.Command, 0)
|
||||
setMetadata, err := c.getOrgMetadataListModelByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgAgg := OrgAggregateFromWriteModel(&setMetadata.WriteModel)
|
||||
for i, data := range metadatas {
|
||||
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 := org.NewMetadataRemovedEvent(ctx, orgAgg, 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.setOrgMetadata(ctx, orgAgg, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events[i] = event
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
// no changes for the metadata
|
||||
if len(events) == 0 {
|
||||
return writeModelToObjectDetails(&setMetadata.WriteModel), nil
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
||||
|
||||
@@ -196,32 +196,6 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) {
|
||||
err: zerrors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid metadata, pre condition error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"ZITADEL",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
metadataList: []*domain.Metadata{
|
||||
{Key: "key"},
|
||||
{Key: "key1"},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add metadata, ok",
|
||||
fields: fields{
|
||||
@@ -234,6 +208,7 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
@@ -274,6 +249,7 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) {
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
@@ -303,6 +279,75 @@ func TestCommandSide_BulkSetOrgMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update and delete metadata, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"ZITADEL",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"key",
|
||||
[]byte("value"),
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"key1",
|
||||
[]byte("value1"),
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"key2",
|
||||
[]byte("value2"),
|
||||
),
|
||||
)),
|
||||
expectPush(
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"key",
|
||||
[]byte("updated_value"),
|
||||
),
|
||||
org.NewMetadataRemovedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"key1",
|
||||
),
|
||||
org.NewMetadataSetEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"key3",
|
||||
[]byte("value3"),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
metadataList: []*domain.Metadata{
|
||||
{Key: "key", Value: []byte("updated_value")}, // update an existing key
|
||||
{Key: "key1"}, // delete an existing key
|
||||
{Key: "key2", Value: []byte("value2")}, // unchanged value
|
||||
{Key: "key3", Value: []byte("value3")}, // add a new key
|
||||
{Key: "key4"}, // delete a non-existing key
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add metadata (no permission), error",
|
||||
fields: fields{
|
||||
|
||||
@@ -76,6 +76,7 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// no changes for the metadata
|
||||
if len(events) == 0 {
|
||||
return writeModelToObjectDetails(&setMetadata.WriteModel), nil
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
|
||||
resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
||||
)
|
||||
|
||||
@@ -176,3 +177,17 @@ func diffProto(expected, actual proto.Message) string {
|
||||
}
|
||||
return "\n\nDiff:\n" + diff
|
||||
}
|
||||
|
||||
// AssertMetadataEquals verifies that two slices of proto Metadata are equal, comparing both length and content by key-value mapping.
|
||||
func AssertMetadataEquals(t assert.TestingT, expected []*metadata.Metadata, actual []*metadata.Metadata) {
|
||||
assert.Equal(t, len(expected), len(actual), "metadata length mismatch")
|
||||
assert.Equal(t, getMetadataMap(expected), getMetadataMap(actual), "metadata content mismatch")
|
||||
}
|
||||
|
||||
func getMetadataMap(metadataEntries []*metadata.Metadata) map[string][]byte {
|
||||
metadataByKey := make(map[string][]byte, len(metadataEntries))
|
||||
for _, md := range metadataEntries {
|
||||
metadataByKey[md.Key] = md.Value
|
||||
}
|
||||
return metadataByKey
|
||||
}
|
||||
|
||||
@@ -392,6 +392,19 @@ func (i *Instance) CreateOrganization(ctx context.Context, name, adminEmail stri
|
||||
return resp
|
||||
}
|
||||
|
||||
func (i *Instance) SetOrganizationMetadata(ctx context.Context, id, key, value string) *org.SetOrganizationMetadataResponse {
|
||||
resp, err := i.Client.OrgV2.SetOrganizationMetadata(ctx, &org.SetOrganizationMetadataRequest{
|
||||
OrganizationId: id,
|
||||
Metadata: []*org.Metadata{{
|
||||
Key: key,
|
||||
Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))),
|
||||
},
|
||||
},
|
||||
})
|
||||
logging.OnError(err).Panic("set organization metadata")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (i *Instance) DeactivateOrganization(ctx context.Context, orgID string) *mgmt.DeactivateOrgResponse {
|
||||
resp, err := i.Client.Mgmt.DeactivateOrg(
|
||||
SetOrgID(ctx, orgID),
|
||||
|
||||
@@ -263,7 +263,7 @@ service OrganizationService {
|
||||
|
||||
// Set Organization Metadata
|
||||
//
|
||||
// Adds or updates a metadata value for the requested key. Make sure the 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 [DeleteOrganizationMetadata](/docs/reference/api/org/zitadel.org.v2.OrganizationService.DeleteOrganizationMetadata). 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:
|
||||
// - `org.write`
|
||||
@@ -914,8 +914,8 @@ message Metadata {
|
||||
// Key is identifier of the metadata entry.
|
||||
string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
|
||||
// Value is the values of the metadata entry.
|
||||
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 SetOrganizationMetadataRequest{
|
||||
// Organization ID is the unique identifier of the organization whose metadata is to be set.
|
||||
|
||||
Reference in New Issue
Block a user