From 2c1f9ac4a862da805c3ed740fe64977d0460af52 Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Tue, 20 Sep 2022 15:32:09 +0100
Subject: [PATCH] feat(org): add org metadata functionality (#4234)
* feat(org): add org metadata functionality
* fix(metadata): add unit tests and review for org metadata
* fix(org-metadata): move endpoints to /
Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com>
---
docs/docs/apis/proto/management.md | 219 ++++++
internal/api/grpc/auth/user.go | 4 +-
internal/api/grpc/management/org.go | 71 ++
internal/api/grpc/management/org_converter.go | 28 +
internal/api/grpc/management/user.go | 6 +-
.../api/grpc/management/user_converter.go | 5 +-
internal/api/grpc/metadata/metadata.go | 27 +-
internal/command/org_metadata.go | 186 +++++
internal/command/org_metadata_model.go | 94 +++
internal/command/org_metadata_test.go | 646 ++++++++++++++++++
internal/command/user_metadata_test.go | 6 +-
internal/query/org_metadata.go | 224 ++++++
internal/query/org_metadata_test.go | 248 +++++++
internal/query/projection/org_metadata.go | 132 ++++
.../query/projection/org_metadata_test.go | 158 +++++
internal/query/projection/projection.go | 2 +
internal/repository/org/eventstore.go | 5 +-
internal/repository/org/metadata.go | 88 +++
internal/repository/org/org.go | 10 +-
internal/static/i18n/de.yaml | 2 +-
internal/static/i18n/en.yaml | 2 +-
internal/static/i18n/fr.yaml | 2 +-
internal/static/i18n/it.yaml | 2 +-
internal/static/i18n/zh.yaml | 2 +-
proto/zitadel/management.proto | 125 ++++
25 files changed, 2267 insertions(+), 27 deletions(-)
create mode 100644 internal/command/org_metadata.go
create mode 100644 internal/command/org_metadata_model.go
create mode 100644 internal/command/org_metadata_test.go
create mode 100644 internal/query/org_metadata.go
create mode 100644 internal/query/org_metadata_test.go
create mode 100644 internal/query/projection/org_metadata.go
create mode 100644 internal/query/projection/org_metadata_test.go
create mode 100644 internal/repository/org/metadata.go
diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md
index 2c369d20a7..5461cb5033 100644
--- a/docs/docs/apis/proto/management.md
+++ b/docs/docs/apis/proto/management.md
@@ -804,6 +804,78 @@ Sets the state of my organisation to active
POST: /orgs/me/_reactivate
+### SetOrgMetadata
+
+> **rpc** SetOrgMetadata([SetOrgMetadataRequest](#setorgmetadatarequest))
+[SetOrgMetadataResponse](#setorgmetadataresponse)
+
+Sets a org metadata by key
+
+
+
+ POST: /metadata/{key}
+
+
+### BulkSetOrgMetadata
+
+> **rpc** BulkSetOrgMetadata([BulkSetOrgMetadataRequest](#bulksetorgmetadatarequest))
+[BulkSetOrgMetadataResponse](#bulksetorgmetadataresponse)
+
+Set a list of org metadata
+
+
+
+ POST: /metadata/_bulk
+
+
+### ListOrgMetadata
+
+> **rpc** ListOrgMetadata([ListOrgMetadataRequest](#listorgmetadatarequest))
+[ListOrgMetadataResponse](#listorgmetadataresponse)
+
+Returns the org metadata
+
+
+
+ POST: /metadata/_search
+
+
+### GetOrgMetadata
+
+> **rpc** GetOrgMetadata([GetOrgMetadataRequest](#getorgmetadatarequest))
+[GetOrgMetadataResponse](#getorgmetadataresponse)
+
+Returns the org metadata by key
+
+
+
+ GET: /metadata/{key}
+
+
+### RemoveOrgMetadata
+
+> **rpc** RemoveOrgMetadata([RemoveOrgMetadataRequest](#removeorgmetadatarequest))
+[RemoveOrgMetadataResponse](#removeorgmetadataresponse)
+
+Removes a org metadata by key
+
+
+
+ DELETE: /metadata/{key}
+
+
+### BulkRemoveOrgMetadata
+
+> **rpc** BulkRemoveOrgMetadata([BulkRemoveOrgMetadataRequest](#bulkremoveorgmetadatarequest))
+[BulkRemoveOrgMetadataResponse](#bulkremoveorgmetadataresponse)
+
+Set a list of org metadata
+
+
+
+ DELETE: /metadata/_bulk
+
+
### ListOrgDomains
> **rpc** ListOrgDomains([ListOrgDomainsRequest](#listorgdomainsrequest))
@@ -3805,6 +3877,28 @@ This is an empty request
+### BulkRemoveOrgMetadataRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| keys | repeated string | - | repeated.items.string.min_len: 1
repeated.items.string.max_len: 200
|
+
+
+
+
+### BulkRemoveOrgMetadataResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### BulkRemoveUserGrantRequest
@@ -3845,6 +3939,40 @@ This is an empty request
+### BulkSetOrgMetadataRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| metadata | repeated BulkSetOrgMetadataRequest.Metadata | - | |
+
+
+
+
+### BulkSetOrgMetadataRequest.Metadata
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| key | string | - | string.min_len: 1
string.max_len: 200
|
+| value | bytes | - | bytes.min_len: 1
bytes.max_len: 500000
|
+
+
+
+
+### BulkSetOrgMetadataResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### BulkSetUserMetadataRequest
@@ -4945,6 +5073,28 @@ This is an empty request
+### GetOrgMetadataRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| key | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### GetOrgMetadataResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| metadata | zitadel.metadata.v1.Metadata | - | |
+
+
+
+
### GetPasswordAgePolicyRequest
This is an empty request
@@ -5774,6 +5924,30 @@ This is an empty request
+### ListOrgMetadataRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| query | zitadel.v1.ListQuery | - | |
+| queries | repeated zitadel.metadata.v1.MetadataQuery | - | |
+
+
+
+
+### ListOrgMetadataResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ListDetails | - | |
+| result | repeated zitadel.metadata.v1.Metadata | - | |
+
+
+
+
### ListPersonalAccessTokensRequest
@@ -6775,6 +6949,28 @@ This is an empty response
+### RemoveOrgMetadataRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| key | string | - | string.min_len: 1
string.max_len: 200
|
+
+
+
+
+### RemoveOrgMetadataResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### RemovePersonalAccessTokenRequest
@@ -7648,6 +7844,29 @@ This is an empty request
+### SetOrgMetadataRequest
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| key | string | - | string.min_len: 1
string.max_len: 200
|
+| value | bytes | - | bytes.min_len: 1
bytes.max_len: 500000
|
+
+
+
+
+### SetOrgMetadataResponse
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| details | zitadel.v1.ObjectDetails | - | |
+
+
+
+
### SetPrimaryOrgDomainRequest
diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go
index e43d018ece..51524d99d4 100644
--- a/internal/api/grpc/auth/user.go
+++ b/internal/api/grpc/auth/user.go
@@ -76,7 +76,7 @@ func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadata
return nil, err
}
return &auth_pb.ListMyMetadataResponse{
- Result: metadata.MetadataListToPb(res.Metadata),
+ Result: metadata.UserMetadataListToPb(res.Metadata),
Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.Timestamp),
}, nil
}
@@ -87,7 +87,7 @@ func (s *Server) GetMyMetadata(ctx context.Context, req *auth_pb.GetMyMetadataRe
return nil, err
}
return &auth_pb.GetMyMetadataResponse{
- Metadata: metadata.DomainMetadataToPb(data),
+ Metadata: metadata.UserMetadataToPb(data),
}, nil
}
diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go
index 62bebb0ffc..b6063929c2 100644
--- a/internal/api/grpc/management/org.go
+++ b/internal/api/grpc/management/org.go
@@ -6,7 +6,9 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
change_grpc "github.com/zitadel/zitadel/internal/api/grpc/change"
member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member"
+ "github.com/zitadel/zitadel/internal/api/grpc/metadata"
"github.com/zitadel/zitadel/internal/api/grpc/object"
+ obj_grpc "github.com/zitadel/zitadel/internal/api/grpc/object"
org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
"github.com/zitadel/zitadel/internal/domain"
@@ -297,3 +299,72 @@ func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, or
}
return userIDs, nil
}
+
+func (s *Server) ListOrgMetadata(ctx context.Context, req *mgmt_pb.ListOrgMetadataRequest) (*mgmt_pb.ListOrgMetadataResponse, error) {
+ metadataQueries, err := ListOrgMetadataToDomain(req)
+ if err != nil {
+ return nil, err
+ }
+ res, err := s.query.SearchOrgMetadata(ctx, true, authz.GetCtxData(ctx).OrgID, metadataQueries)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.ListOrgMetadataResponse{
+ Result: metadata.OrgMetadataListToPb(res.Metadata),
+ Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.Timestamp),
+ }, nil
+}
+
+func (s *Server) GetOrgMetadata(ctx context.Context, req *mgmt_pb.GetOrgMetadataRequest) (*mgmt_pb.GetOrgMetadataResponse, error) {
+ data, err := s.query.GetOrgMetadataByKey(ctx, true, authz.GetCtxData(ctx).OrgID, req.Key)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.GetOrgMetadataResponse{
+ Metadata: metadata.OrgMetadataToPb(data),
+ }, nil
+}
+
+func (s *Server) SetOrgMetadata(ctx context.Context, req *mgmt_pb.SetOrgMetadataRequest) (*mgmt_pb.SetOrgMetadataResponse, error) {
+ result, err := s.command.SetOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, &domain.Metadata{Key: req.Key, Value: req.Value})
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.SetOrgMetadataResponse{
+ Details: obj_grpc.AddToDetailsPb(
+ result.Sequence,
+ result.ChangeDate,
+ result.ResourceOwner,
+ ),
+ }, nil
+}
+
+func (s *Server) BulkSetOrgMetadata(ctx context.Context, req *mgmt_pb.BulkSetOrgMetadataRequest) (*mgmt_pb.BulkSetOrgMetadataResponse, error) {
+ result, err := s.command.BulkSetOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, BulkSetOrgMetadataToDomain(req)...)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.BulkSetOrgMetadataResponse{
+ Details: obj_grpc.DomainToChangeDetailsPb(result),
+ }, nil
+}
+
+func (s *Server) RemoveOrgMetadata(ctx context.Context, req *mgmt_pb.RemoveOrgMetadataRequest) (*mgmt_pb.RemoveOrgMetadataResponse, error) {
+ result, err := s.command.RemoveOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, req.Key)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.RemoveOrgMetadataResponse{
+ Details: obj_grpc.DomainToChangeDetailsPb(result),
+ }, nil
+}
+
+func (s *Server) BulkRemoveOrgMetadata(ctx context.Context, req *mgmt_pb.BulkRemoveOrgMetadataRequest) (*mgmt_pb.BulkRemoveOrgMetadataResponse, error) {
+ result, err := s.command.BulkRemoveOrgMetadata(ctx, authz.GetCtxData(ctx).OrgID, req.Keys...)
+ if err != nil {
+ return nil, err
+ }
+ return &mgmt_pb.BulkRemoveOrgMetadataResponse{
+ Details: obj_grpc.DomainToChangeDetailsPb(result),
+ }, nil
+}
diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go
index e6dc17aef1..299a1abaa9 100644
--- a/internal/api/grpc/management/org_converter.go
+++ b/internal/api/grpc/management/org_converter.go
@@ -5,6 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member"
+ "github.com/zitadel/zitadel/internal/api/grpc/metadata"
"github.com/zitadel/zitadel/internal/api/grpc/object"
org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org"
"github.com/zitadel/zitadel/internal/domain"
@@ -95,3 +96,30 @@ func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembe
OrgID: ctxData.OrgID,
}, nil
}
+
+func BulkSetOrgMetadataToDomain(req *mgmt_pb.BulkSetOrgMetadataRequest) []*domain.Metadata {
+ metadata := make([]*domain.Metadata, len(req.Metadata))
+ for i, data := range req.Metadata {
+ metadata[i] = &domain.Metadata{
+ Key: data.Key,
+ Value: data.Value,
+ }
+ }
+ return metadata
+}
+
+func ListOrgMetadataToDomain(req *mgmt_pb.ListOrgMetadataRequest) (*query.OrgMetadataSearchQueries, error) {
+ offset, limit, asc := object.ListQueryToModel(req.Query)
+ queries, err := metadata.MetadataQueriesToQuery(req.Queries)
+ if err != nil {
+ return nil, err
+ }
+ return &query.OrgMetadataSearchQueries{
+ SearchRequest: query.SearchRequest{
+ Offset: offset,
+ Limit: limit,
+ Asc: asc,
+ },
+ Queries: queries,
+ }, nil
+}
diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go
index 507fcce199..22f051189a 100644
--- a/internal/api/grpc/management/user.go
+++ b/internal/api/grpc/management/user.go
@@ -116,7 +116,7 @@ func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMeta
return nil, err
}
return &mgmt_pb.ListUserMetadataResponse{
- Result: metadata.MetadataListToPb(res.Metadata),
+ Result: metadata.UserMetadataListToPb(res.Metadata),
Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.Timestamp),
}, nil
}
@@ -131,7 +131,7 @@ func (s *Server) GetUserMetadata(ctx context.Context, req *mgmt_pb.GetUserMetada
return nil, err
}
return &mgmt_pb.GetUserMetadataResponse{
- Metadata: metadata.DomainMetadataToPb(data),
+ Metadata: metadata.UserMetadataToPb(data),
}, nil
}
@@ -152,7 +152,7 @@ func (s *Server) SetUserMetadata(ctx context.Context, req *mgmt_pb.SetUserMetada
func (s *Server) BulkSetUserMetadata(ctx context.Context, req *mgmt_pb.BulkSetUserMetadataRequest) (*mgmt_pb.BulkSetUserMetadataResponse, error) {
ctxData := authz.GetCtxData(ctx)
- result, err := s.command.BulkSetUserMetadata(ctx, req.Id, ctxData.OrgID, BulkSetMetadataToDomain(req)...)
+ result, err := s.command.BulkSetUserMetadata(ctx, req.Id, ctxData.OrgID, BulkSetUserMetadataToDomain(req)...)
if err != nil {
return nil, err
}
diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go
index 97ff4ec443..63b2c44b06 100644
--- a/internal/api/grpc/management/user_converter.go
+++ b/internal/api/grpc/management/user_converter.go
@@ -5,9 +5,10 @@ import (
"time"
"github.com/zitadel/logging"
- "github.com/zitadel/zitadel/pkg/grpc/user"
"golang.org/x/text/language"
+ "github.com/zitadel/zitadel/pkg/grpc/user"
+
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/authn"
"github.com/zitadel/zitadel/internal/api/grpc/metadata"
@@ -60,7 +61,7 @@ func UserFieldNameToSortingColumn(field user.UserFieldName) query.Column {
}
}
-func BulkSetMetadataToDomain(req *mgmt_pb.BulkSetUserMetadataRequest) []*domain.Metadata {
+func BulkSetUserMetadataToDomain(req *mgmt_pb.BulkSetUserMetadataRequest) []*domain.Metadata {
metadata := make([]*domain.Metadata, len(req.Metadata))
for i, data := range req.Metadata {
metadata[i] = &domain.Metadata{
diff --git a/internal/api/grpc/metadata/metadata.go b/internal/api/grpc/metadata/metadata.go
index 31ad5c4846..731dda98a8 100644
--- a/internal/api/grpc/metadata/metadata.go
+++ b/internal/api/grpc/metadata/metadata.go
@@ -7,15 +7,36 @@ import (
meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata"
)
-func MetadataListToPb(dataList []*query.UserMetadata) []*meta_pb.Metadata {
+func UserMetadataListToPb(dataList []*query.UserMetadata) []*meta_pb.Metadata {
mds := make([]*meta_pb.Metadata, len(dataList))
for i, data := range dataList {
- mds[i] = DomainMetadataToPb(data)
+ mds[i] = UserMetadataToPb(data)
}
return mds
}
-func DomainMetadataToPb(data *query.UserMetadata) *meta_pb.Metadata {
+func UserMetadataToPb(data *query.UserMetadata) *meta_pb.Metadata {
+ return &meta_pb.Metadata{
+ Key: data.Key,
+ Value: data.Value,
+ Details: object.ToViewDetailsPb(
+ data.Sequence,
+ data.CreationDate,
+ data.ChangeDate,
+ data.ResourceOwner,
+ ),
+ }
+}
+
+func OrgMetadataListToPb(dataList []*query.OrgMetadata) []*meta_pb.Metadata {
+ mds := make([]*meta_pb.Metadata, len(dataList))
+ for i, data := range dataList {
+ mds[i] = OrgMetadataToPb(data)
+ }
+ return mds
+}
+
+func OrgMetadataToPb(data *query.OrgMetadata) *meta_pb.Metadata {
return &meta_pb.Metadata{
Key: data.Key,
Value: data.Value,
diff --git a/internal/command/org_metadata.go b/internal/command/org_metadata.go
new file mode 100644
index 0000000000..c1a3784472
--- /dev/null
+++ b/internal/command/org_metadata.go
@@ -0,0 +1,186 @@
+package command
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ caos_errs "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+func (c *Commands) SetOrgMetadata(ctx context.Context, orgID string, metadata *domain.Metadata) (_ *domain.Metadata, err error) {
+ err = c.checkOrgExists(ctx, orgID)
+ if err != nil {
+ return nil, err
+ }
+ setMetadata := NewOrgMetadataWriteModel(orgID, metadata.Key)
+ orgAgg := OrgAggregateFromWriteModel(&setMetadata.WriteModel)
+ event, err := c.setOrgMetadata(ctx, orgAgg, metadata)
+ if err != nil {
+ return nil, err
+ }
+ pushedEvents, err := c.eventstore.Push(ctx, event)
+ if err != nil {
+ return nil, err
+ }
+
+ err = AppendAndReduce(setMetadata, pushedEvents...)
+ if err != nil {
+ return nil, err
+ }
+ return writeModelToOrgMetadata(setMetadata), nil
+}
+
+func (c *Commands) BulkSetOrgMetadata(ctx context.Context, orgID string, metadatas ...*domain.Metadata) (_ *domain.ObjectDetails, err error) {
+ if len(metadatas) == 0 {
+ return nil, caos_errs.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData")
+ }
+ err = c.checkOrgExists(ctx, orgID)
+ if err != nil {
+ return nil, err
+ }
+
+ events := make([]eventstore.Command, len(metadatas))
+ setMetadata := NewOrgMetadataListWriteModel(orgID)
+ orgAgg := OrgAggregateFromWriteModel(&setMetadata.WriteModel)
+ for i, data := range metadatas {
+ event, err := c.setOrgMetadata(ctx, orgAgg, data)
+ if err != nil {
+ return nil, err
+ }
+ events[i] = event
+ }
+
+ pushedEvents, err := c.eventstore.Push(ctx, events...)
+ if err != nil {
+ return nil, err
+ }
+
+ err = AppendAndReduce(setMetadata, pushedEvents...)
+ if err != nil {
+ return nil, err
+ }
+ return writeModelToObjectDetails(&setMetadata.WriteModel), nil
+}
+
+func (c *Commands) setOrgMetadata(ctx context.Context, orgAgg *eventstore.Aggregate, metadata *domain.Metadata) (command eventstore.Command, err error) {
+ if !metadata.IsValid() {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "META-2ml0f", "Errors.Metadata.Invalid")
+ }
+ return org.NewMetadataSetEvent(
+ ctx,
+ orgAgg,
+ metadata.Key,
+ metadata.Value,
+ ), nil
+}
+
+func (c *Commands) RemoveOrgMetadata(ctx context.Context, orgID, metadataKey string) (_ *domain.ObjectDetails, err error) {
+ if metadataKey == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "META-2n0f1", "Errors.Metadata.Invalid")
+ }
+ err = c.checkOrgExists(ctx, orgID)
+ if err != nil {
+ return nil, err
+ }
+ removeMetadata, err := c.getOrgMetadataModelByID(ctx, orgID, metadataKey)
+ if err != nil {
+ return nil, err
+ }
+ if !removeMetadata.State.Exists() {
+ return nil, caos_errs.ThrowNotFound(nil, "META-mcnw3", "Errors.Metadata.NotFound")
+ }
+ orgAgg := OrgAggregateFromWriteModel(&removeMetadata.WriteModel)
+ event, err := c.removeOrgMetadata(ctx, orgAgg, metadataKey)
+ if err != nil {
+ return nil, err
+ }
+ pushedEvents, err := c.eventstore.Push(ctx, event)
+ if err != nil {
+ return nil, err
+ }
+
+ err = AppendAndReduce(removeMetadata, pushedEvents...)
+ if err != nil {
+ return nil, err
+ }
+ return writeModelToObjectDetails(&removeMetadata.WriteModel), nil
+}
+
+func (c *Commands) BulkRemoveOrgMetadata(ctx context.Context, orgID string, metadataKeys ...string) (_ *domain.ObjectDetails, err error) {
+ if len(metadataKeys) == 0 {
+ return nil, caos_errs.ThrowPreconditionFailed(nil, "META-9mw2d", "Errors.Metadata.NoData")
+ }
+ err = c.checkOrgExists(ctx, orgID)
+ if err != nil {
+ return nil, err
+ }
+
+ events := make([]eventstore.Command, len(metadataKeys))
+ removeMetadata, err := c.getOrgMetadataListModelByID(ctx, orgID)
+ if err != nil {
+ return nil, err
+ }
+ orgAgg := OrgAggregateFromWriteModel(&removeMetadata.WriteModel)
+ for i, key := range metadataKeys {
+ if key == "" {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-m19ds", "Errors.Metadata.Invalid")
+ }
+ if _, found := removeMetadata.metadataList[key]; !found {
+ return nil, caos_errs.ThrowNotFound(nil, "META-2npds", "Errors.Metadata.KeyNotExisting")
+ }
+ event, err := c.removeOrgMetadata(ctx, orgAgg, key)
+ if err != nil {
+ return nil, err
+ }
+ events[i] = event
+ }
+
+ pushedEvents, err := c.eventstore.Push(ctx, events...)
+ if err != nil {
+ return nil, err
+ }
+
+ err = AppendAndReduce(removeMetadata, pushedEvents...)
+ if err != nil {
+ return nil, err
+ }
+ return writeModelToObjectDetails(&removeMetadata.WriteModel), nil
+}
+
+func (c *Commands) removeOrgMetadata(ctx context.Context, orgAgg *eventstore.Aggregate, metadataKey string) (command eventstore.Command, err error) {
+ command = org.NewMetadataRemovedEvent(
+ ctx,
+ orgAgg,
+ metadataKey,
+ )
+ return command, nil
+}
+
+func (c *Commands) getOrgMetadataModelByID(ctx context.Context, orgID, key string) (*OrgMetadataWriteModel, error) {
+ orgMetadataWriteModel := NewOrgMetadataWriteModel(orgID, key)
+ err := c.eventstore.FilterToQueryReducer(ctx, orgMetadataWriteModel)
+ if err != nil {
+ return nil, err
+ }
+ return orgMetadataWriteModel, nil
+}
+
+func (c *Commands) getOrgMetadataListModelByID(ctx context.Context, orgID string) (*OrgMetadataListWriteModel, error) {
+ orgMetadataWriteModel := NewOrgMetadataListWriteModel(orgID)
+ err := c.eventstore.FilterToQueryReducer(ctx, orgMetadataWriteModel)
+ if err != nil {
+ return nil, err
+ }
+ return orgMetadataWriteModel, nil
+}
+
+func writeModelToOrgMetadata(wm *OrgMetadataWriteModel) *domain.Metadata {
+ return &domain.Metadata{
+ ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
+ Key: wm.Key,
+ Value: wm.Value,
+ State: wm.State,
+ }
+}
diff --git a/internal/command/org_metadata_model.go b/internal/command/org_metadata_model.go
new file mode 100644
index 0000000000..7e10387507
--- /dev/null
+++ b/internal/command/org_metadata_model.go
@@ -0,0 +1,94 @@
+package command
+
+import (
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+type OrgMetadataWriteModel struct {
+ MetadataWriteModel
+}
+
+func NewOrgMetadataWriteModel(orgID, key string) *OrgMetadataWriteModel {
+ return &OrgMetadataWriteModel{
+ MetadataWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: orgID,
+ ResourceOwner: orgID,
+ },
+ Key: key,
+ },
+ }
+}
+
+func (wm *OrgMetadataWriteModel) AppendEvents(events ...eventstore.Event) {
+ for _, event := range events {
+ switch e := event.(type) {
+ case *org.MetadataSetEvent:
+ wm.MetadataWriteModel.AppendEvents(&e.SetEvent)
+ case *org.MetadataRemovedEvent:
+ wm.MetadataWriteModel.AppendEvents(&e.RemovedEvent)
+ case *org.MetadataRemovedAllEvent:
+ wm.MetadataWriteModel.AppendEvents(&e.RemovedAllEvent)
+ }
+ }
+}
+
+func (wm *OrgMetadataWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ ResourceOwner(wm.ResourceOwner).
+ AddQuery().
+ AggregateIDs(wm.MetadataWriteModel.AggregateID).
+ AggregateTypes(org.AggregateType).
+ EventTypes(
+ org.MetadataSetType,
+ org.MetadataRemovedType,
+ org.MetadataRemovedAllType).
+ Builder()
+}
+
+type OrgMetadataListWriteModel struct {
+ MetadataListWriteModel
+}
+
+func NewOrgMetadataListWriteModel(orgID string) *OrgMetadataListWriteModel {
+ return &OrgMetadataListWriteModel{
+ MetadataListWriteModel{
+ WriteModel: eventstore.WriteModel{
+ AggregateID: orgID,
+ ResourceOwner: orgID,
+ },
+ metadataList: make(map[string][]byte),
+ },
+ }
+}
+
+func (wm *OrgMetadataListWriteModel) AppendEvents(events ...eventstore.Event) {
+ for _, event := range events {
+ switch e := event.(type) {
+ case *org.MetadataSetEvent:
+ wm.MetadataListWriteModel.AppendEvents(&e.SetEvent)
+ case *org.MetadataRemovedEvent:
+ wm.MetadataListWriteModel.AppendEvents(&e.RemovedEvent)
+ case *org.MetadataRemovedAllEvent:
+ wm.MetadataListWriteModel.AppendEvents(&e.RemovedAllEvent)
+ }
+ }
+}
+
+func (wm *OrgMetadataListWriteModel) Reduce() error {
+ return wm.MetadataListWriteModel.Reduce()
+}
+
+func (wm *OrgMetadataListWriteModel) Query() *eventstore.SearchQueryBuilder {
+ return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
+ ResourceOwner(wm.ResourceOwner).
+ AddQuery().
+ AggregateIDs(wm.MetadataListWriteModel.AggregateID).
+ AggregateTypes(org.AggregateType).
+ EventTypes(
+ org.MetadataSetType,
+ org.MetadataRemovedType,
+ org.MetadataRemovedAllType).
+ Builder()
+}
diff --git a/internal/command/org_metadata_test.go b/internal/command/org_metadata_test.go
new file mode 100644
index 0000000000..468e0dc050
--- /dev/null
+++ b/internal/command/org_metadata_test.go
@@ -0,0 +1,646 @@
+package command
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/domain"
+ caos_errs "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+func TestCommandSide_SetOrgMetadata(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ orgID string
+ metadata *domain.Metadata
+ }
+ )
+ type res struct {
+ want *domain.Metadata
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "org not existing, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadata: &domain.Metadata{
+ Key: "key",
+ Value: []byte("value"),
+ },
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "invalid metadata, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewOrgAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "ZITADEL",
+ ),
+ ),
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadata: &domain.Metadata{
+ Key: "key",
+ },
+ },
+ res: res{
+ err: caos_errs.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "add metadata, ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewOrgAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "ZITADEL",
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ org.NewMetadataSetEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key",
+ []byte("value"),
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadata: &domain.Metadata{
+ Key: "key",
+ Value: []byte("value"),
+ },
+ },
+ res: res{
+ want: &domain.Metadata{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: "org1",
+ ResourceOwner: "org1",
+ },
+ Key: "key",
+ Value: []byte("value"),
+ State: domain.MetadataStateActive,
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ got, err := r.SetOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.metadata)
+ if tt.res.err == nil {
+ assert.NoError(t, err)
+ }
+ if tt.res.err != nil && !tt.res.err(err) {
+ t.Errorf("got wrong err: %v ", err)
+ }
+ if tt.res.err == nil {
+ assert.Equal(t, tt.res.want, got)
+ }
+ })
+ }
+}
+
+func TestCommandSide_BulkSetOrgMetadata(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ orgID string
+ metadataList []*domain.Metadata
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "empty meta data list, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "org not existing, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataList: []*domain.Metadata{
+ {Key: "key", Value: []byte("value")},
+ {Key: "key1", Value: []byte("value1")},
+ },
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "invalid metadata, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ 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: caos_errs.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "add metadata, ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewOrgAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "ZITADEL",
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ org.NewMetadataSetEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key",
+ []byte("value"),
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewMetadataSetEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key1",
+ []byte("value1"),
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataList: []*domain.Metadata{
+ {Key: "key", Value: []byte("value")},
+ {Key: "key1", Value: []byte("value1")},
+ },
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ got, err := r.BulkSetOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.metadataList...)
+ if tt.res.err == nil {
+ assert.NoError(t, err)
+ }
+ if tt.res.err != nil && !tt.res.err(err) {
+ t.Errorf("got wrong err: %v ", err)
+ }
+ if tt.res.err == nil {
+ assert.Equal(t, tt.res.want, got)
+ }
+ })
+ }
+}
+
+func TestCommandSide_OrgRemoveMetadata(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ orgID string
+ metadataKey string
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "org not existing, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataKey: "key",
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "invalid metadata, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataKey: "",
+ },
+ res: res{
+ err: caos_errs.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "meta data not existing, not found error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewOrgAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "ZITADEL",
+ ),
+ ),
+ ),
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataKey: "key",
+ },
+ res: res{
+ err: caos_errs.IsNotFound,
+ },
+ },
+ {
+ name: "remove metadata, ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewOrgAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "ZITADEL",
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewMetadataSetEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key",
+ []byte("value"),
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ org.NewMetadataRemovedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key",
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataKey: "key",
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ got, err := r.RemoveOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.metadataKey)
+ if tt.res.err == nil {
+ assert.NoError(t, err)
+ }
+ if tt.res.err != nil && !tt.res.err(err) {
+ t.Errorf("got wrong err: %v ", err)
+ }
+ if tt.res.err == nil {
+ assert.Equal(t, tt.res.want, got)
+ }
+ })
+ }
+}
+
+func TestCommandSide_BulkRemoveOrgMetadata(t *testing.T) {
+ type fields struct {
+ eventstore *eventstore.Eventstore
+ }
+ type (
+ args struct {
+ ctx context.Context
+ orgID string
+ metadataList []string
+ }
+ )
+ type res struct {
+ want *domain.ObjectDetails
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ name: "empty meta data list, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "org not existing, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataList: []string{"key", "key1"},
+ },
+ res: res{
+ err: caos_errs.IsPreconditionFailed,
+ },
+ },
+ {
+ name: "remove metadata keys not existing, precondition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ expectFilter(
+ eventFromEventPusher(
+ org.NewOrgAddedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "ZITADEL",
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewMetadataSetEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key",
+ []byte("value"),
+ ),
+ ),
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataList: []string{"key", "key1"},
+ },
+ res: res{
+ err: caos_errs.IsNotFound,
+ },
+ },
+ {
+ name: "invalid metadata, pre condition error",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ 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"),
+ ),
+ ),
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataList: []string{""},
+ },
+ res: res{
+ err: caos_errs.IsErrorInvalidArgument,
+ },
+ },
+ {
+ name: "remove metadata, ok",
+ fields: fields{
+ eventstore: eventstoreExpect(
+ t,
+ 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"),
+ ),
+ ),
+ ),
+ expectPush(
+ []*repository.Event{
+ eventFromEventPusher(
+ org.NewMetadataRemovedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key",
+ ),
+ ),
+ eventFromEventPusher(
+ org.NewMetadataRemovedEvent(context.Background(),
+ &org.NewAggregate("org1").Aggregate,
+ "key1",
+ ),
+ ),
+ },
+ ),
+ ),
+ },
+ args: args{
+ ctx: context.Background(),
+ orgID: "org1",
+ metadataList: []string{"key", "key1"},
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Commands{
+ eventstore: tt.fields.eventstore,
+ }
+ got, err := r.BulkRemoveOrgMetadata(tt.args.ctx, tt.args.orgID, tt.args.metadataList...)
+ if tt.res.err == nil {
+ assert.NoError(t, err)
+ }
+ if tt.res.err != nil && !tt.res.err(err) {
+ t.Errorf("got wrong err: %v ", err)
+ }
+ if tt.res.err == nil {
+ assert.Equal(t, tt.res.want, got)
+ }
+ })
+ }
+}
diff --git a/internal/command/user_metadata_test.go b/internal/command/user_metadata_test.go
index ff0f11b4b2..2d480dab92 100644
--- a/internal/command/user_metadata_test.go
+++ b/internal/command/user_metadata_test.go
@@ -15,7 +15,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/user"
)
-func TestCommandSide_SetMetadata(t *testing.T) {
+func TestCommandSide_SetUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
@@ -168,7 +168,7 @@ func TestCommandSide_SetMetadata(t *testing.T) {
}
}
-func TestCommandSide_BulkSetMetadata(t *testing.T) {
+func TestCommandSide_BulkSetUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
@@ -504,7 +504,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) {
}
}
-func TestCommandSide_BulkRemoveMetadata(t *testing.T) {
+func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go
new file mode 100644
index 0000000000..9b2d073b84
--- /dev/null
+++ b/internal/query/org_metadata.go
@@ -0,0 +1,224 @@
+package query
+
+import (
+ "context"
+ "database/sql"
+ errs "errors"
+ "time"
+
+ sq "github.com/Masterminds/squirrel"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/query/projection"
+)
+
+type OrgMetadataList struct {
+ SearchResponse
+ Metadata []*OrgMetadata
+}
+
+type OrgMetadata struct {
+ CreationDate time.Time
+ ChangeDate time.Time
+ ResourceOwner string
+ Sequence uint64
+ Key string
+ Value []byte
+}
+
+type OrgMetadataSearchQueries struct {
+ SearchRequest
+ Queries []SearchQuery
+}
+
+var (
+ orgMetadataTable = table{
+ name: projection.OrgMetadataProjectionTable,
+ }
+ OrgMetadataOrgIDCol = Column{
+ name: projection.OrgMetadataColumnOrgID,
+ table: orgMetadataTable,
+ }
+ OrgMetadataCreationDateCol = Column{
+ name: projection.OrgMetadataColumnCreationDate,
+ table: orgMetadataTable,
+ }
+ OrgMetadataChangeDateCol = Column{
+ name: projection.OrgMetadataColumnChangeDate,
+ table: orgMetadataTable,
+ }
+ OrgMetadataResourceOwnerCol = Column{
+ name: projection.OrgMetadataColumnResourceOwner,
+ table: orgMetadataTable,
+ }
+ OrgMetadataInstanceIDCol = Column{
+ name: projection.OrgMetadataColumnInstanceID,
+ table: orgMetadataTable,
+ }
+ OrgMetadataSequenceCol = Column{
+ name: projection.OrgMetadataColumnSequence,
+ table: orgMetadataTable,
+ }
+ OrgMetadataKeyCol = Column{
+ name: projection.OrgMetadataColumnKey,
+ table: orgMetadataTable,
+ }
+ OrgMetadataValueCol = Column{
+ name: projection.OrgMetadataColumnValue,
+ table: orgMetadataTable,
+ }
+)
+
+func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk bool, orgID string, key string, queries ...SearchQuery) (*OrgMetadata, error) {
+ if shouldTriggerBulk {
+ projection.OrgMetadataProjection.Trigger(ctx)
+ }
+
+ query, scan := prepareOrgMetadataQuery()
+ for _, q := range queries {
+ query = q.toQuery(query)
+ }
+ stmt, args, err := query.Where(
+ sq.Eq{
+ OrgMetadataOrgIDCol.identifier(): orgID,
+ OrgMetadataKeyCol.identifier(): key,
+ OrgMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
+ }).ToSql()
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatment")
+ }
+
+ row := q.client.QueryRowContext(ctx, stmt, args...)
+ return scan(row)
+}
+
+func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool, orgID string, queries *OrgMetadataSearchQueries) (*OrgMetadataList, error) {
+ if shouldTriggerBulk {
+ projection.OrgMetadataProjection.Trigger(ctx)
+ }
+
+ query, scan := prepareOrgMetadataListQuery()
+ stmt, args, err := queries.toQuery(query).Where(
+ sq.Eq{
+ OrgMetadataOrgIDCol.identifier(): orgID,
+ OrgMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
+ }).
+ ToSql()
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatment")
+ }
+
+ rows, err := q.client.QueryContext(ctx, stmt, args...)
+ if err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-Ho2wf", "Errors.Internal")
+ }
+ metadata, err := scan(rows)
+ if err != nil {
+ return nil, err
+ }
+ metadata.LatestSequence, err = q.latestSequence(ctx, orgMetadataTable)
+ return metadata, err
+}
+
+func (q *OrgMetadataSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
+ query = q.SearchRequest.toQuery(query)
+ for _, q := range q.Queries {
+ query = q.toQuery(query)
+ }
+ return query
+}
+
+func (r *OrgMetadataSearchQueries) AppendMyResourceOwnerQuery(orgID string) error {
+ query, err := NewOrgMetadataResourceOwnerSearchQuery(orgID)
+ if err != nil {
+ return err
+ }
+ r.Queries = append(r.Queries, query)
+ return nil
+}
+
+func NewOrgMetadataResourceOwnerSearchQuery(value string) (SearchQuery, error) {
+ return NewTextQuery(OrgMetadataResourceOwnerCol, value, TextEquals)
+}
+
+func NewOrgMetadataKeySearchQuery(value string, comparison TextComparison) (SearchQuery, error) {
+ return NewTextQuery(OrgMetadataKeyCol, value, comparison)
+}
+
+func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, error)) {
+ return sq.Select(
+ OrgMetadataCreationDateCol.identifier(),
+ OrgMetadataChangeDateCol.identifier(),
+ OrgMetadataResourceOwnerCol.identifier(),
+ OrgMetadataSequenceCol.identifier(),
+ OrgMetadataKeyCol.identifier(),
+ OrgMetadataValueCol.identifier(),
+ ).
+ From(orgMetadataTable.identifier()).
+ PlaceholderFormat(sq.Dollar),
+ func(row *sql.Row) (*OrgMetadata, error) {
+ m := new(OrgMetadata)
+ err := row.Scan(
+ &m.CreationDate,
+ &m.ChangeDate,
+ &m.ResourceOwner,
+ &m.Sequence,
+ &m.Key,
+ &m.Value,
+ )
+
+ if err != nil {
+ if errs.Is(err, sql.ErrNoRows) {
+ return nil, errors.ThrowNotFound(err, "QUERY-Rph32", "Errors.Metadata.NotFound")
+ }
+ return nil, errors.ThrowInternal(err, "QUERY-Hajt2", "Errors.Internal")
+ }
+ return m, nil
+ }
+}
+
+func prepareOrgMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*OrgMetadataList, error)) {
+ return sq.Select(
+ OrgMetadataCreationDateCol.identifier(),
+ OrgMetadataChangeDateCol.identifier(),
+ OrgMetadataResourceOwnerCol.identifier(),
+ OrgMetadataSequenceCol.identifier(),
+ OrgMetadataKeyCol.identifier(),
+ OrgMetadataValueCol.identifier(),
+ countColumn.identifier()).
+ From(orgMetadataTable.identifier()).
+ PlaceholderFormat(sq.Dollar),
+ func(rows *sql.Rows) (*OrgMetadataList, error) {
+ metadata := make([]*OrgMetadata, 0)
+ var count uint64
+ for rows.Next() {
+ m := new(OrgMetadata)
+ err := rows.Scan(
+ &m.CreationDate,
+ &m.ChangeDate,
+ &m.ResourceOwner,
+ &m.Sequence,
+ &m.Key,
+ &m.Value,
+ &count,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ metadata = append(metadata, m)
+ }
+
+ if err := rows.Close(); err != nil {
+ return nil, errors.ThrowInternal(err, "QUERY-dd3gh", "Errors.Query.CloseRows")
+ }
+
+ return &OrgMetadataList{
+ Metadata: metadata,
+ SearchResponse: SearchResponse{
+ Count: count,
+ },
+ }, nil
+ }
+}
diff --git a/internal/query/org_metadata_test.go b/internal/query/org_metadata_test.go
new file mode 100644
index 0000000000..eec9f96f0c
--- /dev/null
+++ b/internal/query/org_metadata_test.go
@@ -0,0 +1,248 @@
+package query
+
+import (
+ "database/sql"
+ "database/sql/driver"
+ "errors"
+ "fmt"
+ "regexp"
+ "testing"
+
+ errs "github.com/zitadel/zitadel/internal/errors"
+)
+
+var (
+ orgMetadataQuery = `SELECT projections.org_metadata.creation_date,` +
+ ` projections.org_metadata.change_date,` +
+ ` projections.org_metadata.resource_owner,` +
+ ` projections.org_metadata.sequence,` +
+ ` projections.org_metadata.key,` +
+ ` projections.org_metadata.value` +
+ ` FROM projections.org_metadata`
+ orgMetadataCols = []string{
+ "creation_date",
+ "change_date",
+ "resource_owner",
+ "sequence",
+ "key",
+ "value",
+ }
+ orgMetadataListQuery = `SELECT projections.org_metadata.creation_date,` +
+ ` projections.org_metadata.change_date,` +
+ ` projections.org_metadata.resource_owner,` +
+ ` projections.org_metadata.sequence,` +
+ ` projections.org_metadata.key,` +
+ ` projections.org_metadata.value,` +
+ ` COUNT(*) OVER ()` +
+ ` FROM projections.org_metadata`
+ orgMetadataListCols = []string{
+ "creation_date",
+ "change_date",
+ "resource_owner",
+ "sequence",
+ "key",
+ "value",
+ "count",
+ }
+)
+
+func Test_OrgMetadataPrepares(t *testing.T) {
+ type want struct {
+ sqlExpectations sqlExpectation
+ err checkErr
+ }
+ tests := []struct {
+ name string
+ prepare interface{}
+ want want
+ object interface{}
+ }{
+ {
+ name: "prepareOrgMetadataQuery no result",
+ prepare: prepareOrgMetadataQuery,
+ want: want{
+ sqlExpectations: mockQuery(
+ regexp.QuoteMeta(orgMetadataQuery),
+ nil,
+ nil,
+ ),
+ err: func(err error) (error, bool) {
+ if !errs.IsNotFound(err) {
+ return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: (*OrgMetadata)(nil),
+ },
+ {
+ name: "prepareOrgMetadataQuery found",
+ prepare: prepareOrgMetadataQuery,
+ want: want{
+ sqlExpectations: mockQuery(
+ regexp.QuoteMeta(orgMetadataQuery),
+ orgMetadataCols,
+ []driver.Value{
+ testNow,
+ testNow,
+ "resource_owner",
+ uint64(20211108),
+ "key",
+ []byte("value"),
+ },
+ ),
+ },
+ object: &OrgMetadata{
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ ResourceOwner: "resource_owner",
+ Sequence: 20211108,
+ Key: "key",
+ Value: []byte("value"),
+ },
+ },
+ {
+ name: "prepareOrgMetadataQuery sql err",
+ prepare: prepareOrgMetadataQuery,
+ want: want{
+ sqlExpectations: mockQueryErr(
+ regexp.QuoteMeta(orgMetadataQuery),
+ sql.ErrConnDone,
+ ),
+ err: func(err error) (error, bool) {
+ if !errors.Is(err, sql.ErrConnDone) {
+ return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: nil,
+ },
+ {
+ name: "prepareOrgMetadataListQuery no result",
+ prepare: prepareOrgMetadataListQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(orgMetadataListQuery),
+ nil,
+ nil,
+ ),
+ err: func(err error) (error, bool) {
+ if !errs.IsNotFound(err) {
+ return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: &OrgMetadataList{Metadata: []*OrgMetadata{}},
+ },
+ {
+ name: "prepareOrgMetadataListQuery one result",
+ prepare: prepareOrgMetadataListQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(orgMetadataListQuery),
+ orgMetadataListCols,
+ [][]driver.Value{
+ {
+ testNow,
+ testNow,
+ "resource_owner",
+ uint64(20211108),
+ "key",
+ []byte("value"),
+ },
+ },
+ ),
+ },
+ object: &OrgMetadataList{
+ SearchResponse: SearchResponse{
+ Count: 1,
+ },
+ Metadata: []*OrgMetadata{
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ ResourceOwner: "resource_owner",
+ Sequence: 20211108,
+ Key: "key",
+ Value: []byte("value"),
+ },
+ },
+ },
+ },
+ {
+ name: "prepareOrgMetadataListQuery multiple results",
+ prepare: prepareOrgMetadataListQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(orgMetadataListQuery),
+ orgMetadataListCols,
+ [][]driver.Value{
+ {
+ testNow,
+ testNow,
+ "resource_owner",
+ uint64(20211108),
+ "key",
+ []byte("value"),
+ },
+ {
+ testNow,
+ testNow,
+ "resource_owner",
+ uint64(20211108),
+ "key2",
+ []byte("value2"),
+ },
+ },
+ ),
+ },
+ object: &OrgMetadataList{
+ SearchResponse: SearchResponse{
+ Count: 2,
+ },
+ Metadata: []*OrgMetadata{
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ ResourceOwner: "resource_owner",
+ Sequence: 20211108,
+ Key: "key",
+ Value: []byte("value"),
+ },
+ {
+ CreationDate: testNow,
+ ChangeDate: testNow,
+ ResourceOwner: "resource_owner",
+ Sequence: 20211108,
+ Key: "key2",
+ Value: []byte("value2"),
+ },
+ },
+ },
+ },
+ {
+ name: "prepareOrgMetadataListQuery sql err",
+ prepare: prepareOrgMetadataListQuery,
+ want: want{
+ sqlExpectations: mockQueryErr(
+ regexp.QuoteMeta(orgMetadataListQuery),
+ sql.ErrConnDone,
+ ),
+ err: func(err error) (error, bool) {
+ if !errors.Is(err, sql.ErrConnDone) {
+ return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
+ }
+ return nil, true
+ },
+ },
+ object: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
+ })
+ }
+}
diff --git a/internal/query/projection/org_metadata.go b/internal/query/projection/org_metadata.go
new file mode 100644
index 0000000000..095b393793
--- /dev/null
+++ b/internal/query/projection/org_metadata.go
@@ -0,0 +1,132 @@
+package projection
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+const (
+ OrgMetadataProjectionTable = "projections.org_metadata"
+
+ OrgMetadataColumnOrgID = "org_id"
+ OrgMetadataColumnCreationDate = "creation_date"
+ OrgMetadataColumnChangeDate = "change_date"
+ OrgMetadataColumnSequence = "sequence"
+ OrgMetadataColumnResourceOwner = "resource_owner"
+ OrgMetadataColumnInstanceID = "instance_id"
+ OrgMetadataColumnKey = "key"
+ OrgMetadataColumnValue = "value"
+)
+
+type orgMetadataProjection struct {
+ crdb.StatementHandler
+}
+
+func newOrgMetadataProjection(ctx context.Context, config crdb.StatementHandlerConfig) *orgMetadataProjection {
+ p := new(orgMetadataProjection)
+ config.ProjectionName = OrgMetadataProjectionTable
+ config.Reducers = p.reducers()
+ config.InitCheck = crdb.NewTableCheck(
+ crdb.NewTable([]*crdb.Column{
+ crdb.NewColumn(OrgMetadataColumnOrgID, crdb.ColumnTypeText),
+ crdb.NewColumn(OrgMetadataColumnCreationDate, crdb.ColumnTypeTimestamp),
+ crdb.NewColumn(OrgMetadataColumnChangeDate, crdb.ColumnTypeTimestamp),
+ crdb.NewColumn(OrgMetadataColumnSequence, crdb.ColumnTypeInt64),
+ crdb.NewColumn(OrgMetadataColumnResourceOwner, crdb.ColumnTypeText),
+ crdb.NewColumn(OrgMetadataColumnInstanceID, crdb.ColumnTypeText),
+ crdb.NewColumn(OrgMetadataColumnKey, crdb.ColumnTypeText),
+ crdb.NewColumn(OrgMetadataColumnValue, crdb.ColumnTypeBytes, crdb.Nullable()),
+ },
+ crdb.NewPrimaryKey(OrgMetadataColumnInstanceID, OrgMetadataColumnOrgID, OrgMetadataColumnKey),
+ ),
+ )
+
+ p.StatementHandler = crdb.NewStatementHandler(ctx, config)
+ return p
+}
+
+func (p *orgMetadataProjection) reducers() []handler.AggregateReducer {
+ return []handler.AggregateReducer{
+ {
+ Aggregate: org.AggregateType,
+ EventRedusers: []handler.EventReducer{
+ {
+ Event: org.MetadataSetType,
+ Reduce: p.reduceMetadataSet,
+ },
+ {
+ Event: org.MetadataRemovedType,
+ Reduce: p.reduceMetadataRemoved,
+ },
+ {
+ Event: org.MetadataRemovedAllType,
+ Reduce: p.reduceMetadataRemovedAll,
+ },
+ {
+ Event: org.OrgRemovedEventType,
+ Reduce: p.reduceMetadataRemovedAll,
+ },
+ },
+ },
+ }
+}
+
+func (p *orgMetadataProjection) reduceMetadataSet(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*org.MetadataSetEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Ghn53", "reduce.wrong.event.type %s", org.MetadataSetType)
+ }
+ return crdb.NewUpsertStatement(
+ e,
+ []handler.Column{
+ handler.NewCol(OrgMetadataColumnInstanceID, nil),
+ handler.NewCol(OrgMetadataColumnOrgID, nil),
+ handler.NewCol(OrgMetadataColumnKey, e.Key),
+ },
+ []handler.Column{
+ handler.NewCol(OrgMetadataColumnInstanceID, e.Aggregate().InstanceID),
+ handler.NewCol(OrgMetadataColumnOrgID, e.Aggregate().ID),
+ handler.NewCol(OrgMetadataColumnKey, e.Key),
+ handler.NewCol(OrgMetadataColumnResourceOwner, e.Aggregate().ResourceOwner),
+ handler.NewCol(OrgMetadataColumnCreationDate, e.CreationDate()),
+ handler.NewCol(OrgMetadataColumnChangeDate, e.CreationDate()),
+ handler.NewCol(OrgMetadataColumnSequence, e.Sequence()),
+ handler.NewCol(OrgMetadataColumnValue, e.Value),
+ },
+ ), nil
+}
+
+func (p *orgMetadataProjection) reduceMetadataRemoved(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*org.MetadataRemovedEvent)
+ if !ok {
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Bm542", "reduce.wrong.event.type %s", org.MetadataRemovedType)
+ }
+ return crdb.NewDeleteStatement(
+ e,
+ []handler.Condition{
+ handler.NewCond(OrgMetadataColumnOrgID, e.Aggregate().ID),
+ handler.NewCond(OrgMetadataColumnKey, e.Key),
+ },
+ ), nil
+}
+
+func (p *orgMetadataProjection) reduceMetadataRemovedAll(event eventstore.Event) (*handler.Statement, error) {
+ switch event.(type) {
+ case *org.MetadataRemovedAllEvent,
+ *org.OrgRemovedEvent:
+ //ok
+ default:
+ return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Bmnf3", "reduce.wrong.event.type %v", []eventstore.EventType{org.MetadataRemovedAllType, org.OrgRemovedEventType})
+ }
+ return crdb.NewDeleteStatement(
+ event,
+ []handler.Condition{
+ handler.NewCond(OrgMetadataColumnOrgID, event.Aggregate().ID),
+ },
+ ), nil
+}
diff --git a/internal/query/projection/org_metadata_test.go b/internal/query/projection/org_metadata_test.go
new file mode 100644
index 0000000000..a37e90660c
--- /dev/null
+++ b/internal/query/projection/org_metadata_test.go
@@ -0,0 +1,158 @@
+package projection
+
+import (
+ "testing"
+
+ "github.com/zitadel/zitadel/internal/errors"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/handler"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/org"
+)
+
+func TestOrgMetadataProjection_reduces(t *testing.T) {
+ type args struct {
+ event func(t *testing.T) eventstore.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ reduce func(event eventstore.Event) (*handler.Statement, error)
+ want wantReduce
+ }{
+ {
+ name: "reduceMetadataSet",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.MetadataSetType),
+ org.AggregateType,
+ []byte(`{
+ "key": "key",
+ "value": "dmFsdWU="
+ }`),
+ ), org.MetadataSetEventMapper),
+ },
+ reduce: (&orgMetadataProjection{}).reduceMetadataSet,
+ want: wantReduce{
+ aggregateType: org.AggregateType,
+ sequence: 15,
+ previousSequence: 10,
+ projection: OrgMetadataProjectionTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "INSERT INTO projections.org_metadata (instance_id, org_id, key, resource_owner, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, org_id, key) DO UPDATE SET (resource_owner, creation_date, change_date, sequence, value) = (EXCLUDED.resource_owner, EXCLUDED.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ "agg-id",
+ "key",
+ "ro-id",
+ anyArg{},
+ anyArg{},
+ uint64(15),
+ []byte("value"),
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceMetadataRemoved",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.MetadataRemovedType),
+ org.AggregateType,
+ []byte(`{
+ "key": "key"
+ }`),
+ ), org.MetadataRemovedEventMapper),
+ },
+ reduce: (&orgMetadataProjection{}).reduceMetadataRemoved,
+ want: wantReduce{
+ aggregateType: org.AggregateType,
+ sequence: 15,
+ previousSequence: 10,
+ projection: OrgMetadataProjectionTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.org_metadata WHERE (org_id = $1) AND (key = $2)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ "key",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceMetadataRemovedAll",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.MetadataRemovedAllType),
+ org.AggregateType,
+ nil,
+ ), org.MetadataRemovedAllEventMapper),
+ },
+ reduce: (&orgMetadataProjection{}).reduceMetadataRemovedAll,
+ want: wantReduce{
+ aggregateType: org.AggregateType,
+ sequence: 15,
+ previousSequence: 10,
+ projection: OrgMetadataProjectionTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.org_metadata WHERE (org_id = $1)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "reduceMetadataRemovedAll (org removed)",
+ args: args{
+ event: getEvent(testEvent(
+ repository.EventType(org.OrgRemovedEventType),
+ org.AggregateType,
+ nil,
+ ), org.OrgRemovedEventMapper),
+ },
+ reduce: (&orgMetadataProjection{}).reduceMetadataRemovedAll,
+ want: wantReduce{
+ aggregateType: org.AggregateType,
+ sequence: 15,
+ previousSequence: 10,
+ projection: OrgMetadataProjectionTable,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.org_metadata WHERE (org_id = $1)",
+ expectedArgs: []interface{}{
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ event := baseEvent(t)
+ got, err := tt.reduce(event)
+ if _, ok := err.(errors.InvalidArgument); !ok {
+ t.Errorf("no wrong event mapping: %v, got: %v", err, got)
+ }
+
+ event = tt.args.event(t)
+ got, err = tt.reduce(event)
+ assertReduce(t, got, err, tt.want)
+ })
+ }
+}
diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go
index 4495fcb352..803298390b 100644
--- a/internal/query/projection/projection.go
+++ b/internal/query/projection/projection.go
@@ -20,6 +20,7 @@ const (
var (
projectionConfig crdb.StatementHandlerConfig
OrgProjection *orgProjection
+ OrgMetadataProjection *orgMetadataProjection
ActionProjection *actionProjection
FlowProjection *flowProjection
ProjectProjection *projectProjection
@@ -82,6 +83,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co
}
OrgProjection = newOrgProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["orgs"]))
+ OrgMetadataProjection = newOrgMetadataProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["org_metadata"]))
ActionProjection = newActionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["actions"]))
FlowProjection = newFlowProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["flows"]))
ProjectProjection = newProjectProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects"]))
diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go
index 6b1bd0da5c..38a9ccb9d0 100644
--- a/internal/repository/org/eventstore.go
+++ b/internal/repository/org/eventstore.go
@@ -79,5 +79,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(IDPJWTConfigChangedEventType, IDPJWTConfigChangedEventMapper).
RegisterFilterEventMapper(TriggerActionsSetEventType, TriggerActionsSetEventMapper).
RegisterFilterEventMapper(TriggerActionsCascadeRemovedEventType, TriggerActionsCascadeRemovedEventMapper).
- RegisterFilterEventMapper(FlowClearedEventType, FlowClearedEventMapper)
+ RegisterFilterEventMapper(FlowClearedEventType, FlowClearedEventMapper).
+ RegisterFilterEventMapper(MetadataSetType, MetadataSetEventMapper).
+ RegisterFilterEventMapper(MetadataRemovedType, MetadataRemovedEventMapper).
+ RegisterFilterEventMapper(MetadataRemovedAllType, MetadataRemovedAllEventMapper)
}
diff --git a/internal/repository/org/metadata.go b/internal/repository/org/metadata.go
new file mode 100644
index 0000000000..04bc64f89b
--- /dev/null
+++ b/internal/repository/org/metadata.go
@@ -0,0 +1,88 @@
+package org
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/repository/metadata"
+)
+
+const (
+ MetadataSetType = orgEventTypePrefix + metadata.SetEventType
+ MetadataRemovedType = orgEventTypePrefix + metadata.RemovedEventType
+ MetadataRemovedAllType = orgEventTypePrefix + metadata.RemovedAllEventType
+)
+
+type MetadataSetEvent struct {
+ metadata.SetEvent
+}
+
+func NewMetadataSetEvent(ctx context.Context, aggregate *eventstore.Aggregate, key string, value []byte) *MetadataSetEvent {
+ return &MetadataSetEvent{
+ SetEvent: *metadata.NewSetEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ MetadataSetType),
+ key,
+ value),
+ }
+}
+
+func MetadataSetEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := metadata.SetEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &MetadataSetEvent{SetEvent: *e.(*metadata.SetEvent)}, nil
+}
+
+type MetadataRemovedEvent struct {
+ metadata.RemovedEvent
+}
+
+func NewMetadataRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, key string) *MetadataRemovedEvent {
+ return &MetadataRemovedEvent{
+ RemovedEvent: *metadata.NewRemovedEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ MetadataRemovedType),
+ key),
+ }
+}
+
+func MetadataRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := metadata.RemovedEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &MetadataRemovedEvent{RemovedEvent: *e.(*metadata.RemovedEvent)}, nil
+}
+
+type MetadataRemovedAllEvent struct {
+ metadata.RemovedAllEvent
+}
+
+func NewMetadataRemovedAllEvent(ctx context.Context, aggregate *eventstore.Aggregate) *MetadataRemovedAllEvent {
+ return &MetadataRemovedAllEvent{
+ RemovedAllEvent: *metadata.NewRemovedAllEvent(
+ eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ MetadataRemovedAllType),
+ ),
+ }
+}
+
+func MetadataRemovedAllEventMapper(event *repository.Event) (eventstore.Event, error) {
+ e, err := metadata.RemovedAllEventMapper(event)
+ if err != nil {
+ return nil, err
+ }
+
+ return &MetadataRemovedAllEvent{RemovedAllEvent: *e.(*metadata.RemovedAllEvent)}, nil
+}
diff --git a/internal/repository/org/org.go b/internal/repository/org/org.go
index 6666f04ad6..0bd04365c8 100644
--- a/internal/repository/org/org.go
+++ b/internal/repository/org/org.go
@@ -191,13 +191,7 @@ func NewOrgRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, na
}
func OrgRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
- orgChanged := &OrgRemovedEvent{
+ return &OrgRemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
- }
- err := json.Unmarshal(event.Data, orgChanged)
- if err != nil {
- return nil, errors.ThrowInternal(err, "ORG-DAfbs", "unable to unmarshal org deactivated")
- }
-
- return orgChanged, nil
+ }, nil
}
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index c7f09e72d1..09a812a784 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -394,7 +394,7 @@ Errors:
ReadError: Übersetzungsdatei konnte nicht gelesen werden
MergeError: Übersetzungsdatei konnte nicht mit benutzerdefinierten Übersetzungen zusammengeführt werden
NotFound: Übersetzungsdatei existiert nicht
- MetaData:
+ Metadata:
NotFound: Meta Daten konnten nicht gefunden werden
NoData: Meta Daten Liste ist leer
Invalid: Meta Daten sind ungültig
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index 488f51e9d9..bca370719e 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -394,7 +394,7 @@ Errors:
ReadError: Error in reading translation file
MergeError: Translation file could not be merged with custom translations
NotFound: Translation file doesn't exist
- MetaData:
+ Metadata:
NotFound: Metadata not found
NoData: Metadata list is empty
Invalid: Metadata is invalid
diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml
index c9d3a1b79a..cdb0a61d03 100644
--- a/internal/static/i18n/fr.yaml
+++ b/internal/static/i18n/fr.yaml
@@ -394,7 +394,7 @@ Errors:
ReadError: Erreur de lecture du fichier de traduction
MergeError: Le fichier de traduction n'a pas pu être fusionné avec les traductions personnalisées.
NotFound: Le fichier de traduction n'existe pas
- MetaData:
+ Metadata:
NotFound: Métadonnées non trouvées
NoData: La liste des métadonnées est vide
Invalid: Les métadonnées ne sont pas valides
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index af9527ec25..cfbddcd88c 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -394,7 +394,7 @@ Errors:
ReadError: Errore nella lettura del file di traduzione
MergeError: Il file di traduzione non può essere unito alle traduzioni personalizzate
NotFound: Il file di traduzione non esiste
- MetaData:
+ Metadata:
NotFound: Metadati non trovati
NoData: L'elenco dei metadati è vuoto
Invalid: I metadati non sono validi
diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml
index c436010f74..9d145da32e 100644
--- a/internal/static/i18n/zh.yaml
+++ b/internal/static/i18n/zh.yaml
@@ -388,7 +388,7 @@ Errors:
ReadError: 读取翻译文件时出错
MergeError: 翻译文件无法与自定义翻译合并
NotFound: 翻译文件不存在
- MetaData:
+ Metadata:
NotFound: 元数据不存在
NoData: 元数据列表为空
Invalid: 元数据无效
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index 65f9350d71..a0226dd2c6 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -841,6 +841,76 @@ service ManagementService {
};
}
+ // Sets a org metadata by key
+ rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) {
+ option (google.api.http) = {
+ post: "/metadata/{key}"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.write"
+ };
+ }
+
+ // Set a list of org metadata
+ rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) {
+ option (google.api.http) = {
+ post: "/metadata/_bulk"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.write"
+ };
+ }
+
+ // Returns the org metadata
+ rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) {
+ option (google.api.http) = {
+ post: "/metadata/_search"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.read"
+ };
+ }
+
+ // Returns the org metadata by key
+ rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) {
+ option (google.api.http) = {
+ get: "/metadata/{key}"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.read"
+ };
+ }
+
+ // Removes a org metadata by key
+ rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) {
+ option (google.api.http) = {
+ delete: "/metadata/{key}"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.write"
+ };
+ }
+
+ // Set a list of org metadata
+ rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) {
+ option (google.api.http) = {
+ delete: "/metadata/_bulk"
+ body: "*"
+ };
+
+ option (zitadel.v1.auth_option) = {
+ permission: "org.write"
+ };
+ }
+
// Returns all registered domains of my organisation
// Limit should always be set, there is a default limit set by the service
rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) {
@@ -3680,6 +3750,61 @@ message RemoveOrgMemberResponse {
zitadel.v1.ObjectDetails details = 1;
}
+message ListOrgMetadataRequest {
+ zitadel.v1.ListQuery query = 1;
+ repeated zitadel.metadata.v1.MetadataQuery queries = 2;
+}
+
+message ListOrgMetadataResponse {
+ zitadel.v1.ListDetails details = 1;
+ repeated zitadel.metadata.v1.Metadata result = 2;
+}
+
+message GetOrgMetadataRequest {
+ string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetOrgMetadataResponse {
+ zitadel.metadata.v1.Metadata metadata = 1;
+}
+
+message SetOrgMetadataRequest {
+ string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
+}
+
+message SetOrgMetadataResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
+message BulkSetOrgMetadataRequest {
+ message Metadata {
+ string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+ bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}];
+ }
+ repeated Metadata metadata = 1;
+}
+
+message BulkSetOrgMetadataResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
+message RemoveOrgMetadataRequest {
+ string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message RemoveOrgMetadataResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
+message BulkRemoveOrgMetadataRequest {
+ repeated string keys = 1 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}];
+}
+
+message BulkRemoveOrgMetadataResponse {
+ zitadel.v1.ObjectDetails details = 1;
+}
+
message GetProjectByIDRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}