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}]; }