From 1367a2e139db5db7cb6d475c6fe4b401d00414e1 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 2 Feb 2022 09:04:05 +0100 Subject: [PATCH] feat: limit amount of active actions (#3143) * max actions * fix: max allowed actions * fix: max allowed actions * fix tests --- docs/docs/apis/proto/admin.md | 4 + docs/docs/apis/proto/management.md | 24 +++ internal/api/grpc/admin/features.go | 14 +- internal/api/grpc/features/features.go | 30 ++- .../eventstore/token_verifier.go | 2 +- internal/command/features_model.go | 14 +- internal/command/iam_converter.go | 3 +- internal/command/iam_features.go | 3 +- internal/command/iam_features_model.go | 12 +- internal/command/org_action.go | 60 +++++- internal/command/org_action_model.go | 19 +- internal/command/org_action_test.go | 195 +++++++++++++++++- internal/command/org_features.go | 32 ++- internal/command/org_features_model.go | 12 +- internal/command/org_features_test.go | 12 +- internal/domain/action.go | 8 + internal/domain/features.go | 3 +- internal/query/action_flow.go | 13 +- internal/query/action_flow_test.go | 27 +++ internal/query/features.go | 19 +- internal/query/features_test.go | 30 ++- internal/query/projection/feature.go | 17 +- internal/query/projection/feature_test.go | 24 ++- internal/repository/features/features.go | 56 ++--- internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/ui/login/handler/custom_action.go | 6 +- .../V1.109__features_max_actions.sql | 5 + proto/zitadel/admin.proto | 4 + proto/zitadel/features.proto | 8 + proto/zitadel/management.proto | 47 +++-- 32 files changed, 583 insertions(+), 123 deletions(-) create mode 100644 migrations/cockroach/V1.109__features_max_actions.sql diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 294e11705..8d680af7d 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -2564,6 +2564,8 @@ This is an empty request | custom_text_login | bool | - | | | lockout_policy | bool | - | | | actions | bool | - | | +| actions_allowed | zitadel.features.v1.ActionsAllowed | - | | +| max_actions | int32 | - | | @@ -2754,6 +2756,8 @@ This is an empty request | custom_text_login | bool | - | | | lockout_policy | bool | - | | | actions | bool | - | | +| actions_allowed | zitadel.features.v1.ActionsAllowed | - | | +| max_actions | int32 | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index ed4b19e6f..7e44631fc 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -2758,6 +2758,30 @@ Change JWT identity provider configuration of the organisation PUT: /actions/{id} +### DeactivateAction + +> **rpc** DeactivateAction([DeactivateActionRequest](#deactivateactionrequest)) +[DeactivateActionResponse](#deactivateactionresponse) + + + + + + POST: /actions/{id}/_deactivate + + +### ReactivateAction + +> **rpc** ReactivateAction([ReactivateActionRequest](#reactivateactionrequest)) +[ReactivateActionResponse](#reactivateactionresponse) + + + + + + POST: /actions/{id}/_reactivate + + ### DeleteAction > **rpc** DeleteAction([DeleteActionRequest](#deleteactionrequest)) diff --git a/internal/api/grpc/admin/features.go b/internal/api/grpc/admin/features.go index 412653e26..b93927e18 100644 --- a/internal/api/grpc/admin/features.go +++ b/internal/api/grpc/admin/features.go @@ -60,6 +60,10 @@ func (s *Server) ResetOrgFeatures(ctx context.Context, req *admin_pb.ResetOrgFea } func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) *domain.Features { + actionsAllowed := features_grpc.ActionsAllowedToDomain(req.ActionsAllowed) + if req.Actions { + actionsAllowed = domain.ActionsAllowedUnlimited + } return &domain.Features{ TierName: req.TierName, TierDescription: req.Description, @@ -79,11 +83,16 @@ func setDefaultFeaturesRequestToDomain(req *admin_pb.SetDefaultFeaturesRequest) CustomTextLogin: req.CustomTextLogin || req.CustomText, CustomTextMessage: req.CustomTextMessage, LockoutPolicy: req.LockoutPolicy, - Actions: req.Actions, + ActionsAllowed: actionsAllowed, + MaxActions: int(req.MaxActions), } } func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain.Features { + actionsAllowed := features_grpc.ActionsAllowedToDomain(req.ActionsAllowed) + if req.Actions { + actionsAllowed = domain.ActionsAllowedUnlimited + } return &domain.Features{ TierName: req.TierName, TierDescription: req.Description, @@ -105,6 +114,7 @@ func setOrgFeaturesRequestToDomain(req *admin_pb.SetOrgFeaturesRequest) *domain. CustomTextLogin: req.CustomTextLogin || req.CustomText, CustomTextMessage: req.CustomTextMessage, LockoutPolicy: req.LockoutPolicy, - Actions: req.Actions, + ActionsAllowed: actionsAllowed, + MaxActions: int(req.MaxActions), } } diff --git a/internal/api/grpc/features/features.go b/internal/api/grpc/features/features.go index 24026f671..077142d86 100644 --- a/internal/api/grpc/features/features.go +++ b/internal/api/grpc/features/features.go @@ -31,7 +31,9 @@ func ModelFeaturesToPb(features *query.Features) *features_pb.Features { CustomTextLogin: features.CustomTextLogin, MetadataUser: features.MetadataUser, LockoutPolicy: features.LockoutPolicy, - Actions: features.Actions, + Actions: features.ActionsAllowed != domain.ActionsNotAllowed, + ActionsAllowed: ActionsAllowedToPb(features.ActionsAllowed), + MaxActions: features.MaxActions, Details: object_grpc.ChangeToDetailsPb( features.Sequence, features.ChangeDate, @@ -78,3 +80,29 @@ func FeaturesStateToDomain(status features_pb.FeaturesState) domain.FeaturesStat return -1 } } + +func ActionsAllowedToDomain(allowed features_pb.ActionsAllowed) domain.ActionsAllowed { + switch allowed { + case features_pb.ActionsAllowed_ACTIONS_ALLOWED_NOT_ALLOWED: + return domain.ActionsNotAllowed + case features_pb.ActionsAllowed_ACTIONS_ALLOWED_MAX: + return domain.ActionsMaxAllowed + case features_pb.ActionsAllowed_ACTIONS_ALLOWED_UNLIMITED: + return domain.ActionsAllowedUnlimited + default: + return domain.ActionsNotAllowed + } +} + +func ActionsAllowedToPb(allowed domain.ActionsAllowed) features_pb.ActionsAllowed { + switch allowed { + case domain.ActionsNotAllowed: + return features_pb.ActionsAllowed_ACTIONS_ALLOWED_NOT_ALLOWED + case domain.ActionsMaxAllowed: + return features_pb.ActionsAllowed_ACTIONS_ALLOWED_MAX + case domain.ActionsAllowedUnlimited: + return features_pb.ActionsAllowed_ACTIONS_ALLOWED_UNLIMITED + default: + return features_pb.ActionsAllowed_ACTIONS_ALLOWED_NOT_ALLOWED + } +} diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index a1c967e36..c2538e0de 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -168,7 +168,7 @@ func checkFeatures(features *query.Features, requiredFeatures ...string) error { continue } if requiredFeature == domain.FeatureActions { - if !features.Actions { + if features.ActionsAllowed == domain.ActionsNotAllowed { return MissingFeatureErr(requiredFeature) } continue diff --git a/internal/command/features_model.go b/internal/command/features_model.go index 7fcad1acb..81a499081 100644 --- a/internal/command/features_model.go +++ b/internal/command/features_model.go @@ -31,7 +31,8 @@ type FeaturesWriteModel struct { CustomTextMessage bool CustomTextLogin bool LockoutPolicy bool - Actions bool + ActionsAllowed domain.ActionsAllowed + MaxActions int } func (wm *FeaturesWriteModel) Reduce() error { @@ -103,7 +104,16 @@ func (wm *FeaturesWriteModel) Reduce() error { wm.LockoutPolicy = *e.LockoutPolicy } if e.Actions != nil { - wm.Actions = *e.Actions + wm.ActionsAllowed = domain.ActionsNotAllowed + if *e.Actions { + wm.ActionsAllowed = domain.ActionsAllowedUnlimited + } + } + if e.ActionsAllowed != nil { + wm.ActionsAllowed = *e.ActionsAllowed + } + if e.MaxActions != nil { + wm.MaxActions = *e.MaxActions } case *features.FeaturesRemovedEvent: wm.State = domain.FeaturesStateRemoved diff --git a/internal/command/iam_converter.go b/internal/command/iam_converter.go index 34c8b9ec5..9d03c7bc2 100644 --- a/internal/command/iam_converter.go +++ b/internal/command/iam_converter.go @@ -195,6 +195,7 @@ func writeModelToFeatures(wm *FeaturesWriteModel) *domain.Features { CustomTextMessage: wm.CustomTextMessage, CustomTextLogin: wm.CustomTextLogin, LockoutPolicy: wm.LockoutPolicy, - Actions: wm.Actions, + ActionsAllowed: wm.ActionsAllowed, + MaxActions: wm.MaxActions, } } diff --git a/internal/command/iam_features.go b/internal/command/iam_features.go index 25cee7fa8..ed61d28ba 100644 --- a/internal/command/iam_features.go +++ b/internal/command/iam_features.go @@ -55,7 +55,8 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *IAM features.CustomTextMessage, features.CustomTextLogin, features.LockoutPolicy, - features.Actions, + features.ActionsAllowed, + features.MaxActions, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") diff --git a/internal/command/iam_features_model.go b/internal/command/iam_features_model.go index 61f1dc536..71251ab4a 100644 --- a/internal/command/iam_features_model.go +++ b/internal/command/iam_features_model.go @@ -72,8 +72,9 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( metadataUser, customTextMessage, customTextLogin, - lockoutPolicy, - actions bool, + lockoutPolicy bool, + actionsAllowed domain.ActionsAllowed, + maxActions int, ) (*iam.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -138,8 +139,11 @@ func (wm *IAMFeaturesWriteModel) NewSetEvent( if wm.LockoutPolicy != lockoutPolicy { changes = append(changes, features.ChangeLockoutPolicy(lockoutPolicy)) } - if wm.Actions != actions { - changes = append(changes, features.ChangeActions(actions)) + if wm.ActionsAllowed != actionsAllowed { + changes = append(changes, features.ChangeActionsAllowed(actionsAllowed)) + } + if wm.MaxActions != maxActions { + changes = append(changes, features.ChangeMaxActions(maxActions)) } if len(changes) == 0 { return nil, false diff --git a/internal/command/org_action.go b/internal/command/org_action.go index b1bcb048c..6e5c219e7 100644 --- a/internal/command/org_action.go +++ b/internal/command/org_action.go @@ -2,6 +2,7 @@ package command import ( "context" + "sort" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" @@ -14,6 +15,10 @@ func (c *Commands) AddAction(ctx context.Context, addAction *domain.Action, reso if !addAction.IsValid() { return "", nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-eg2gf", "Errors.Action.Invalid") } + err = c.checkAdditionalActionAllowed(ctx, resourceOwner) + if err != nil { + return "", nil, err + } addAction.AggregateID, err = c.idGenerator.Next() if err != nil { return "", nil, err @@ -39,6 +44,27 @@ func (c *Commands) AddAction(ctx context.Context, addAction *domain.Action, reso return actionModel.AggregateID, writeModelToObjectDetails(&actionModel.WriteModel), nil } +func (c *Commands) checkAdditionalActionAllowed(ctx context.Context, resourceOwner string) error { + features, err := c.getOrgFeaturesOrDefault(ctx, resourceOwner) + if err != nil { + return err + } + existingActions, err := c.getActionsByOrgWriteModelByID(ctx, resourceOwner) + if err != nil { + return err + } + activeActions := make([]*ActionWriteModel, 0, len(existingActions.Actions)) + for _, existingAction := range existingActions.Actions { + if existingAction.State == domain.ActionStateActive { + activeActions = append(activeActions, existingAction) + } + } + if len(activeActions) < features.MaxActions { + return nil + } + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-dfwg2", "Errors.Action.MaxAllowed") +} + func (c *Commands) ChangeAction(ctx context.Context, actionChange *domain.Action, resourceOwner string) (*domain.ObjectDetails, error) { if !actionChange.IsValid() || actionChange.AggregateID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Df2f3", "Errors.Action.Invalid") @@ -119,6 +145,11 @@ func (c *Commands) ReactivateAction(ctx context.Context, actionID string, resour if existingAction.State != domain.ActionStateInactive { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-J53zh", "Errors.Action.NotInactive") } + err = c.checkAdditionalActionAllowed(ctx, resourceOwner) + if err != nil { + return nil, err + } + actionAgg := ActionAggregateFromWriteModel(&existingAction.WriteModel) events := []eventstore.Command{ action.NewReactivatedEvent(ctx, actionAgg), @@ -174,9 +205,34 @@ func (c *Commands) removeActionsFromOrg(ctx context.Context, resourceOwner strin return nil, nil } events := make([]eventstore.Command, 0, len(existingActions.Actions)) - for id, name := range existingActions.Actions { + for id, existingAction := range existingActions.Actions { actionAgg := NewActionAggregate(id, resourceOwner) - events = append(events, action.NewRemovedEvent(ctx, actionAgg, name)) + events = append(events, action.NewRemovedEvent(ctx, actionAgg, existingAction.Name)) + } + return events, nil +} + +func (c *Commands) deactivateNotAllowedActionsFromOrg(ctx context.Context, resourceOwner string, maxAllowed int) ([]eventstore.Command, error) { + existingActions, err := c.getActionsByOrgWriteModelByID(ctx, resourceOwner) + if err != nil { + return nil, err + } + activeActions := make([]*ActionWriteModel, 0, len(existingActions.Actions)) + for _, existingAction := range existingActions.Actions { + if existingAction.State == domain.ActionStateActive { + activeActions = append(activeActions, existingAction) + } + } + if len(activeActions) <= maxAllowed { + return nil, nil + } + sort.Slice(activeActions, func(i, j int) bool { + return activeActions[i].WriteModel.ChangeDate.Before(activeActions[j].WriteModel.ChangeDate) + }) + events := make([]eventstore.Command, 0, len(existingActions.Actions)) + for i := maxAllowed; i < len(activeActions); i++ { + actionAgg := NewActionAggregate(activeActions[i].AggregateID, resourceOwner) + events = append(events, action.NewDeactivatedEvent(ctx, actionAgg)) } return events, nil } diff --git a/internal/command/org_action_model.go b/internal/command/org_action_model.go index 07b3cac52..3eb3caea9 100644 --- a/internal/command/org_action_model.go +++ b/internal/command/org_action_model.go @@ -159,7 +159,7 @@ func (wm *ActionExistsModel) Query() *eventstore.SearchQueryBuilder { type ActionsListByOrgModel struct { eventstore.WriteModel - Actions map[string]string + Actions map[string]*ActionWriteModel } func NewActionsListByOrgModel(resourceOwner string) *ActionsListByOrgModel { @@ -167,7 +167,7 @@ func NewActionsListByOrgModel(resourceOwner string) *ActionsListByOrgModel { WriteModel: eventstore.WriteModel{ ResourceOwner: resourceOwner, }, - Actions: make(map[string]string), + Actions: make(map[string]*ActionWriteModel), } } @@ -175,7 +175,18 @@ func (wm *ActionsListByOrgModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *action.AddedEvent: - wm.Actions[e.Aggregate().ID] = e.Name + wm.Actions[e.Aggregate().ID] = &ActionWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: e.Aggregate().ID, + ChangeDate: e.CreationDate(), + }, + Name: e.Name, + State: domain.ActionStateActive, + } + case *action.DeactivatedEvent: + wm.Actions[e.Aggregate().ID].State = domain.ActionStateInactive + case *action.ReactivatedEvent: + wm.Actions[e.Aggregate().ID].State = domain.ActionStateActive case *action.RemovedEvent: delete(wm.Actions, e.Aggregate().ID) } @@ -189,6 +200,8 @@ func (wm *ActionsListByOrgModel) Query() *eventstore.SearchQueryBuilder { AddQuery(). AggregateTypes(action.AggregateType). EventTypes(action.AddedEventType, + action.DeactivatedEventType, + action.ReactivatedEventType, action.RemovedEventType). Builder() } diff --git a/internal/command/org_action_test.go b/internal/command/org_action_test.go index 2d1558b01..1d3d42b86 100644 --- a/internal/command/org_action_test.go +++ b/internal/command/org_action_test.go @@ -4,6 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/repository/features" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -13,7 +17,6 @@ import ( "github.com/caos/zitadel/internal/id/mock" "github.com/caos/zitadel/internal/repository/action" "github.com/caos/zitadel/internal/repository/org" - "github.com/stretchr/testify/assert" ) func TestCommands_AddAction(t *testing.T) { @@ -53,10 +56,76 @@ func TestCommands_AddAction(t *testing.T) { err: errors.IsErrorInvalidArgument, }, }, + { + "no additional allowed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewFeaturesSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []features.FeaturesChanges{ + features.ChangeMaxActions(1), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + action.NewAddedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + "name", + "name() {};", + 0, + false, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + addAction: &domain.Action{ + Name: "name", + Script: "name() {};", + }, + resourceOwner: "org1", + }, + res{ + err: errors.IsPreconditionFailed, + }, + }, { "unique constraint failed, error", fields{ eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewFeaturesSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []features.FeaturesChanges{ + features.ChangeMaxActions(2), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + action.NewAddedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + "name", + "name() {};", + 0, + false, + ), + ), + ), expectPushFailed( errors.ThrowPreconditionFailed(nil, "id", "name already exists"), []*repository.Event{ @@ -91,33 +160,57 @@ func TestCommands_AddAction(t *testing.T) { "push ok", fields{ eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewFeaturesSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []features.FeaturesChanges{ + features.ChangeMaxActions(2), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + action.NewAddedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + "name", + "name() {};", + 0, + false, + ), + ), + ), expectPush( []*repository.Event{ eventFromEventPusher( action.NewAddedEvent(context.Background(), - &action.NewAggregate("id1", "org1").Aggregate, - "name", - "name() {};", + &action.NewAggregate("id2", "org1").Aggregate, + "name2", + "name2() {};", 0, false, ), ), }, - uniqueConstraintsFromEventConstraint(action.NewAddActionNameUniqueConstraint("name", "org1")), + uniqueConstraintsFromEventConstraint(action.NewAddActionNameUniqueConstraint("name2", "org1")), ), ), - idGenerator: mock.ExpectID(t, "id1"), + idGenerator: mock.ExpectID(t, "id2"), }, args{ ctx: context.Background(), addAction: &domain.Action{ - Name: "name", - Script: "name() {};", + Name: "name2", + Script: "name2() {};", }, resourceOwner: "org1", }, res{ - id: "id1", + id: "id2", details: &domain.ObjectDetails{ ResourceOwner: "org1", }, @@ -570,10 +663,94 @@ func TestCommands_ReactivateAction(t *testing.T) { err: errors.IsPreconditionFailed, }, }, + { + "no additional allowed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + action.NewAddedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + "name", + "name() {};", + 0, + false, + ), + ), + eventFromEventPusher( + action.NewDeactivatedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + ), + ), + ), + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewFeaturesSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []features.FeaturesChanges{ + features.ChangeMaxActions(1), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + action.NewAddedEvent(context.Background(), + &action.NewAggregate("id2", "org1").Aggregate, + "name2", + "name2() {};", + 0, + false, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + actionID: "id1", + resourceOwner: "org1", + }, + res{ + err: errors.IsPreconditionFailed, + }, + }, { "reactivate ok", fields{ eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + action.NewAddedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + "name", + "name() {};", + 0, + false, + ), + ), + eventFromEventPusher( + action.NewDeactivatedEvent(context.Background(), + &action.NewAggregate("id1", "org1").Aggregate, + ), + ), + ), + expectFilter( + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewFeaturesSetEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + []features.FeaturesChanges{ + features.ChangeMaxActions(1), + }, + ) + return e + }(), + ), + ), expectFilter( eventFromEventPusher( action.NewAddedEvent(context.Background(), diff --git a/internal/command/org_features.go b/internal/command/org_features.go index eab002554..9dc00b0a4 100644 --- a/internal/command/org_features.go +++ b/internal/command/org_features.go @@ -45,7 +45,8 @@ func (c *Commands) SetOrgFeatures(ctx context.Context, resourceOwner string, fea features.CustomTextMessage, features.CustomTextLogin, features.LockoutPolicy, - features.Actions, + features.ActionsAllowed, + features.MaxActions, ) if !hasChanged { return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged") @@ -177,7 +178,7 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string events = append(events, removeOrgUserMetadatas...) } } - if !features.Actions { + if features.ActionsAllowed == domain.ActionsNotAllowed { removeOrgActions, err := c.removeActionsFromOrg(ctx, orgID) if err != nil { return nil, err @@ -186,6 +187,15 @@ func (c *Commands) ensureOrgSettingsToFeatures(ctx context.Context, orgID string events = append(events, removeOrgActions...) } } + if features.ActionsAllowed == domain.ActionsMaxAllowed { + deactivateActions, err := c.deactivateNotAllowedActionsFromOrg(ctx, orgID, features.MaxActions) + if err != nil { + return nil, err + } + if len(deactivateActions) > 0 { + events = append(events, deactivateActions...) + } + } return events, nil } @@ -351,3 +361,21 @@ func (c *Commands) setAllowedLabelPolicy(ctx context.Context, orgID string, feat } return events, nil } + +func (c *Commands) getOrgFeaturesOrDefault(ctx context.Context, orgID string) (*domain.Features, error) { + existingFeatures := NewOrgFeaturesWriteModel(orgID) + err := c.eventstore.FilterToQueryReducer(ctx, existingFeatures) + if err != nil { + return nil, err + } + if existingFeatures.State != domain.FeaturesStateUnspecified && existingFeatures.State != domain.FeaturesStateRemoved { + return writeModelToFeatures(&existingFeatures.FeaturesWriteModel), nil + } + + existingIAMFeatures := NewIAMFeaturesWriteModel() + err = c.eventstore.FilterToQueryReducer(ctx, existingIAMFeatures) + if err != nil { + return nil, err + } + return writeModelToFeatures(&existingIAMFeatures.FeaturesWriteModel), nil +} diff --git a/internal/command/org_features_model.go b/internal/command/org_features_model.go index 7e8b52464..ca9224dbc 100644 --- a/internal/command/org_features_model.go +++ b/internal/command/org_features_model.go @@ -78,8 +78,9 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( metadataUser, customTextMessage, customTextLogin, - lockoutPolicy, - actions bool, + lockoutPolicy bool, + actionsAllowed domain.ActionsAllowed, + maxActions int, ) (*org.FeaturesSetEvent, bool) { changes := make([]features.FeaturesChanges, 0) @@ -144,8 +145,11 @@ func (wm *OrgFeaturesWriteModel) NewSetEvent( if wm.LockoutPolicy != lockoutPolicy { changes = append(changes, features.ChangeLockoutPolicy(lockoutPolicy)) } - if wm.Actions != actions { - changes = append(changes, features.ChangeActions(actions)) + if wm.ActionsAllowed != actionsAllowed { + changes = append(changes, features.ChangeActionsAllowed(actionsAllowed)) + } + if wm.MaxActions != maxActions { + changes = append(changes, features.ChangeMaxActions(maxActions)) } if len(changes) == 0 { diff --git a/internal/command/org_features_test.go b/internal/command/org_features_test.go index da01a76f5..1d0352998 100644 --- a/internal/command/org_features_test.go +++ b/internal/command/org_features_test.go @@ -308,7 +308,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { PrivacyPolicy: false, MetadataUser: false, LockoutPolicy: false, - Actions: false, + ActionsAllowed: domain.ActionsNotAllowed, }, }, res: res{ @@ -512,7 +512,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { MetadataUser: false, PrivacyPolicy: false, LockoutPolicy: false, - Actions: false, + ActionsAllowed: domain.ActionsNotAllowed, }, }, res: res{ @@ -726,7 +726,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { MetadataUser: false, PrivacyPolicy: false, LockoutPolicy: false, - Actions: false, + ActionsAllowed: domain.ActionsNotAllowed, }, }, res: res{ @@ -950,7 +950,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { MetadataUser: false, PrivacyPolicy: false, LockoutPolicy: false, - Actions: false, + ActionsAllowed: domain.ActionsNotAllowed, }, }, res: res{ @@ -1243,7 +1243,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { MetadataUser: false, PrivacyPolicy: false, LockoutPolicy: false, - Actions: false, + ActionsAllowed: domain.ActionsNotAllowed, }, }, res: res{ @@ -1433,7 +1433,7 @@ func TestCommandSide_SetOrgFeatures(t *testing.T) { PrivacyPolicy: false, MetadataUser: false, LockoutPolicy: false, - Actions: false, + ActionsAllowed: domain.ActionsNotAllowed, }, }, res: res{ diff --git a/internal/domain/action.go b/internal/domain/action.go index 8bfd80b90..7e69404b3 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -37,3 +37,11 @@ func (s ActionState) Valid() bool { func (s ActionState) Exists() bool { return s != ActionStateUnspecified && s != ActionStateRemoved } + +type ActionsAllowed int32 + +const ( + ActionsNotAllowed ActionsAllowed = iota + ActionsMaxAllowed + ActionsAllowedUnlimited +) diff --git a/internal/domain/features.go b/internal/domain/features.go index 68a85c512..dabf098aa 100644 --- a/internal/domain/features.go +++ b/internal/domain/features.go @@ -54,7 +54,8 @@ type Features struct { PrivacyPolicy bool MetadataUser bool LockoutPolicy bool - Actions bool + ActionsAllowed ActionsAllowed + MaxActions int } type FeaturesState int32 diff --git a/internal/query/action_flow.go b/internal/query/action_flow.go index 273aafde4..c2285659b 100644 --- a/internal/query/action_flow.go +++ b/internal/query/action_flow.go @@ -67,13 +67,14 @@ func (q *Queries) GetFlow(ctx context.Context, flowType domain.FlowType, orgID s return scan(rows) } -func (q *Queries) GetActionsByFlowAndTriggerType(ctx context.Context, flowType domain.FlowType, triggerType domain.TriggerType, orgID string) ([]*Action, error) { +func (q *Queries) GetActiveActionsByFlowAndTriggerType(ctx context.Context, flowType domain.FlowType, triggerType domain.TriggerType, orgID string) ([]*Action, error) { stmt, scan := prepareTriggerActionsQuery() query, args, err := stmt.Where( sq.Eq{ FlowsTriggersColumnFlowType.identifier(): flowType, FlowsTriggersColumnTriggerType.identifier(): triggerType, FlowsTriggersColumnResourceOwner.identifier(): orgID, + ActionColumnState.identifier(): domain.ActionStateActive, }, ).ToSql() if err != nil { @@ -135,7 +136,7 @@ func prepareTriggerActionsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*Action, ActionColumnCreationDate.identifier(), ActionColumnChangeDate.identifier(), ActionColumnResourceOwner.identifier(), - //ActionColumnState.identifier(), + ActionColumnState.identifier(), ActionColumnSequence.identifier(), ActionColumnName.identifier(), ActionColumnScript.identifier(), @@ -152,7 +153,7 @@ func prepareTriggerActionsQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*Action, &action.CreationDate, &action.ChangeDate, &action.ResourceOwner, - //&action.State, //TODO: state in next release + &action.State, &action.Sequence, &action.Name, &action.Script, @@ -177,7 +178,7 @@ func prepareFlowQuery() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { ActionColumnCreationDate.identifier(), ActionColumnChangeDate.identifier(), ActionColumnResourceOwner.identifier(), - //ActionColumnState.identifier(), + ActionColumnState.identifier(), ActionColumnSequence.identifier(), ActionColumnName.identifier(), ActionColumnScript.identifier(), @@ -199,6 +200,7 @@ func prepareFlowQuery() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { actionCreationDate pq.NullTime actionChangeDate pq.NullTime actionResourceOwner sql.NullString + actionState sql.NullInt32 actionSequence sql.NullInt64 actionName sql.NullString actionScript sql.NullString @@ -212,7 +214,7 @@ func prepareFlowQuery() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { &actionCreationDate, &actionChangeDate, &actionResourceOwner, - //&action.State, //TODO: state in next release + &actionState, &actionSequence, &actionName, &actionScript, @@ -232,6 +234,7 @@ func prepareFlowQuery() (sq.SelectBuilder, func(*sql.Rows) (*Flow, error)) { CreationDate: actionCreationDate.Time, ChangeDate: actionChangeDate.Time, ResourceOwner: actionResourceOwner.String, + State: domain.ActionState(actionState.Int32), Sequence: uint64(actionSequence.Int64), Name: actionName.String, Script: actionScript.String, diff --git a/internal/query/action_flow_test.go b/internal/query/action_flow_test.go index 9b9963479..924678fdb 100644 --- a/internal/query/action_flow_test.go +++ b/internal/query/action_flow_test.go @@ -31,6 +31,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script,`+ @@ -54,6 +55,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script,`+ @@ -67,6 +69,7 @@ func Test_FlowPrepares(t *testing.T) { "creation_date", "change_date", "resource_owner", + "state", "sequence", "name", "script", @@ -80,6 +83,7 @@ func Test_FlowPrepares(t *testing.T) { testNow, testNow, "ro", + domain.ActionStateActive, uint64(20211115), "action-name", "script", @@ -99,6 +103,7 @@ func Test_FlowPrepares(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", + State: domain.ActionStateActive, Sequence: 20211115, Name: "action-name", Script: "script", @@ -116,6 +121,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script,`+ @@ -129,6 +135,7 @@ func Test_FlowPrepares(t *testing.T) { "creation_date", "change_date", "resource_owner", + "state", "sequence", "name", "script", @@ -142,6 +149,7 @@ func Test_FlowPrepares(t *testing.T) { testNow, testNow, "ro", + domain.ActionStateActive, uint64(20211115), "action-name-pre", "script", @@ -154,6 +162,7 @@ func Test_FlowPrepares(t *testing.T) { testNow, testNow, "ro", + domain.ActionStateActive, uint64(20211115), "action-name-post", "script", @@ -173,6 +182,7 @@ func Test_FlowPrepares(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", + State: domain.ActionStateActive, Sequence: 20211115, Name: "action-name-pre", Script: "script", @@ -184,6 +194,7 @@ func Test_FlowPrepares(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", + State: domain.ActionStateActive, Sequence: 20211115, Name: "action-name-post", Script: "script", @@ -201,6 +212,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script,`+ @@ -214,6 +226,7 @@ func Test_FlowPrepares(t *testing.T) { "creation_date", "change_date", "resource_owner", + "state", "sequence", "name", "script", @@ -230,6 +243,7 @@ func Test_FlowPrepares(t *testing.T) { nil, nil, nil, + nil, domain.TriggerTypePostCreation, uint64(20211109), domain.FlowTypeExternalAuthentication, @@ -251,6 +265,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script,`+ @@ -279,6 +294,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script`+ @@ -299,6 +315,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script`+ @@ -309,6 +326,7 @@ func Test_FlowPrepares(t *testing.T) { "creation_date", "change_date", "resource_owner", + "state", "sequence", "name", "script", @@ -319,6 +337,7 @@ func Test_FlowPrepares(t *testing.T) { testNow, testNow, "ro", + domain.AddressStateActive, uint64(20211115), "action-name", "script", @@ -332,6 +351,7 @@ func Test_FlowPrepares(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", + State: domain.ActionStateActive, Sequence: 20211115, Name: "action-name", Script: "script", @@ -347,6 +367,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script`+ @@ -357,6 +378,7 @@ func Test_FlowPrepares(t *testing.T) { "creation_date", "change_date", "resource_owner", + "state", "sequence", "name", "script", @@ -367,6 +389,7 @@ func Test_FlowPrepares(t *testing.T) { testNow, testNow, "ro", + domain.AddressStateActive, uint64(20211115), "action-name-1", "script", @@ -376,6 +399,7 @@ func Test_FlowPrepares(t *testing.T) { testNow, testNow, "ro", + domain.ActionStateActive, uint64(20211115), "action-name-2", "script", @@ -389,6 +413,7 @@ func Test_FlowPrepares(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", + State: domain.ActionStateActive, Sequence: 20211115, Name: "action-name-1", Script: "script", @@ -398,6 +423,7 @@ func Test_FlowPrepares(t *testing.T) { CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", + State: domain.ActionStateActive, Sequence: 20211115, Name: "action-name-2", Script: "script", @@ -413,6 +439,7 @@ func Test_FlowPrepares(t *testing.T) { ` zitadel.projections.actions.creation_date,`+ ` zitadel.projections.actions.change_date,`+ ` zitadel.projections.actions.resource_owner,`+ + ` zitadel.projections.actions.action_state,`+ ` zitadel.projections.actions.sequence,`+ ` zitadel.projections.actions.name,`+ ` zitadel.projections.actions.script`+ diff --git a/internal/query/features.go b/internal/query/features.go index dba30e3cf..6d74cd98c 100644 --- a/internal/query/features.go +++ b/internal/query/features.go @@ -38,7 +38,8 @@ type Features struct { CustomTextMessage bool CustomTextLogin bool LockoutPolicy bool - Actions bool + ActionsAllowed domain.ActionsAllowed + MaxActions int32 } var ( @@ -141,8 +142,12 @@ var ( name: projection.FeatureLockoutPolicyCol, table: featureTable, } - FeatureActions = Column{ - name: projection.FeatureActionsCol, + FeatureActionsAllowed = Column{ + name: projection.FeatureActionsAllowedCol, + table: featureTable, + } + FeatureMaxActions = Column{ + name: projection.FeatureMaxActionsCol, table: featureTable, } ) @@ -207,7 +212,8 @@ func prepareFeaturesQuery() (sq.SelectBuilder, func(*sql.Row) (*Features, error) FeatureCustomTextMessage.identifier(), FeatureCustomTextLogin.identifier(), FeatureLockoutPolicy.identifier(), - FeatureActions.identifier(), + FeatureActionsAllowed.identifier(), + FeatureMaxActions.identifier(), ).From(featureTable.identifier()).PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Features, error) { p := new(Features) @@ -239,7 +245,8 @@ func prepareFeaturesQuery() (sq.SelectBuilder, func(*sql.Row) (*Features, error) &p.CustomTextMessage, &p.CustomTextLogin, &p.LockoutPolicy, - &p.Actions, + &p.ActionsAllowed, + &p.MaxActions, ) if err != nil { if errs.Is(err, sql.ErrNoRows) { @@ -301,7 +308,7 @@ func (f *Features) EnabledFeatureTypes() []string { if f.LockoutPolicy { list = append(list, domain.FeatureLockoutPolicy) } - if f.Actions { + if f.ActionsAllowed != domain.ActionsNotAllowed { list = append(list, domain.FeatureActions) } return list diff --git a/internal/query/features_test.go b/internal/query/features_test.go index c3ad3248c..4709f6f50 100644 --- a/internal/query/features_test.go +++ b/internal/query/features_test.go @@ -53,7 +53,8 @@ func Test_FeaturesPrepares(t *testing.T) { ` zitadel.projections.features.custom_text_message,`+ ` zitadel.projections.features.custom_text_login,`+ ` zitadel.projections.features.lockout_policy,`+ - ` zitadel.projections.features.actions`+ + ` zitadel.projections.features.actions_allowed,`+ + ` zitadel.projections.features.max_actions`+ ` FROM zitadel.projections.features`), nil, nil, @@ -96,7 +97,8 @@ func Test_FeaturesPrepares(t *testing.T) { ` zitadel.projections.features.custom_text_message,`+ ` zitadel.projections.features.custom_text_login,`+ ` zitadel.projections.features.lockout_policy,`+ - ` zitadel.projections.features.actions`+ + ` zitadel.projections.features.actions_allowed,`+ + ` zitadel.projections.features.max_actions`+ ` FROM zitadel.projections.features`), []string{ "aggregate_id", @@ -123,7 +125,8 @@ func Test_FeaturesPrepares(t *testing.T) { "custom_text_message", "custom_text_login", "lockout_policy", - "actions", + "actions_allowed", + "max_actions", }, []driver.Value{ "aggregate-id", @@ -150,7 +153,8 @@ func Test_FeaturesPrepares(t *testing.T) { true, true, true, - true, + domain.ActionsMaxAllowed, + 10, }, ), }, @@ -179,7 +183,8 @@ func Test_FeaturesPrepares(t *testing.T) { CustomTextMessage: true, CustomTextLogin: true, LockoutPolicy: true, - Actions: true, + ActionsAllowed: domain.ActionsMaxAllowed, + MaxActions: 10, }, }, { @@ -211,7 +216,8 @@ func Test_FeaturesPrepares(t *testing.T) { ` zitadel.projections.features.custom_text_message,`+ ` zitadel.projections.features.custom_text_login,`+ ` zitadel.projections.features.lockout_policy,`+ - ` zitadel.projections.features.actions`+ + ` zitadel.projections.features.actions_allowed,`+ + ` zitadel.projections.features.max_actions`+ ` FROM zitadel.projections.features`), []string{ "aggregate_id", @@ -238,7 +244,8 @@ func Test_FeaturesPrepares(t *testing.T) { "custom_text_message", "custom_text_login", "lockout_policy", - "actions", + "actions_allowed", + "max_actions", }, []driver.Value{ "aggregate-id", @@ -265,7 +272,8 @@ func Test_FeaturesPrepares(t *testing.T) { true, true, true, - true, + domain.ActionsMaxAllowed, + 10, }, ), }, @@ -294,7 +302,8 @@ func Test_FeaturesPrepares(t *testing.T) { CustomTextMessage: true, CustomTextLogin: true, LockoutPolicy: true, - Actions: true, + ActionsAllowed: domain.ActionsMaxAllowed, + MaxActions: 10, }, }, { @@ -326,7 +335,8 @@ func Test_FeaturesPrepares(t *testing.T) { ` zitadel.projections.features.custom_text_message,`+ ` zitadel.projections.features.custom_text_login,`+ ` zitadel.projections.features.lockout_policy,`+ - ` zitadel.projections.features.actions`+ + ` zitadel.projections.features.actions_allowed,`+ + ` zitadel.projections.features.max_actions`+ ` FROM zitadel.projections.features`), sql.ErrConnDone, ), diff --git a/internal/query/projection/feature.go b/internal/query/projection/feature.go index 6791fda8d..b77b17534 100644 --- a/internal/query/projection/feature.go +++ b/internal/query/projection/feature.go @@ -5,6 +5,8 @@ import ( "github.com/caos/logging" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore/handler" @@ -82,7 +84,8 @@ const ( FeatureCustomTextMessageCol = "custom_text_message" FeatureCustomTextLoginCol = "custom_text_login" FeatureLockoutPolicyCol = "lockout_policy" - FeatureActionsCol = "actions" + FeatureActionsAllowedCol = "actions_allowed" + FeatureMaxActionsCol = "max_actions" ) func (p *FeatureProjection) reduceFeatureSet(event eventstore.Event) (*handler.Statement, error) { @@ -173,7 +176,17 @@ func (p *FeatureProjection) reduceFeatureSet(event eventstore.Event) (*handler.S cols = append(cols, handler.NewCol(FeatureLockoutPolicyCol, *featureEvent.LockoutPolicy)) } if featureEvent.Actions != nil { - cols = append(cols, handler.NewCol(FeatureActionsCol, *featureEvent.Actions)) + actionsAllowed := domain.ActionsNotAllowed + if *featureEvent.Actions { + actionsAllowed = domain.ActionsAllowedUnlimited + } + cols = append(cols, handler.NewCol(FeatureActionsAllowedCol, actionsAllowed)) + } + if featureEvent.ActionsAllowed != nil { + cols = append(cols, handler.NewCol(FeatureActionsAllowedCol, *featureEvent.ActionsAllowed)) + } + if featureEvent.MaxActions != nil { + cols = append(cols, handler.NewCol(FeatureMaxActionsCol, *featureEvent.MaxActions)) } return crdb.NewUpsertStatement( &featureEvent, diff --git a/internal/query/projection/feature_test.go b/internal/query/projection/feature_test.go index f7c750717..3ccf26b5a 100644 --- a/internal/query/projection/feature_test.go +++ b/internal/query/projection/feature_test.go @@ -50,7 +50,8 @@ func TestFeatureProjection_reduces(t *testing.T) { "customTextMessage": true, "customTextLogin": true, "lockoutPolicy": true, - "actions": true + "actionsAllowed": 1, + "maxActions": 10 }`), ), org.FeaturesSetEventMapper), }, @@ -63,7 +64,7 @@ func TestFeatureProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", + expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions_allowed, max_actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -89,7 +90,8 @@ func TestFeatureProjection_reduces(t *testing.T) { true, true, true, - true, + domain.ActionsMaxAllowed, + 10, }, }, }, @@ -136,7 +138,7 @@ func TestFeatureProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", + expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions_allowed) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -162,7 +164,7 @@ func TestFeatureProjection_reduces(t *testing.T) { true, true, true, - true, + domain.ActionsAllowedUnlimited, }, }, }, @@ -266,7 +268,7 @@ func TestFeatureProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", + expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions_allowed) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -292,7 +294,7 @@ func TestFeatureProjection_reduces(t *testing.T) { true, true, true, - true, + domain.ActionsAllowedUnlimited, }, }, }, @@ -327,7 +329,8 @@ func TestFeatureProjection_reduces(t *testing.T) { "customTextMessage": true, "customTextLogin": true, "lockoutPolicy": true, - "actions": true + "actionsAllowed": 1, + "maxActions": 10 }`), ), iam.FeaturesSetEventMapper), }, @@ -339,7 +342,7 @@ func TestFeatureProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)", + expectedStmt: "UPSERT INTO zitadel.projections.features (aggregate_id, change_date, sequence, is_default, tier_name, tier_description, state, state_description, audit_log_retention, login_policy_factors, login_policy_idp, login_policy_passwordless, login_policy_registration, login_policy_username_login, login_policy_password_reset, password_complexity_policy, label_policy_private_label, label_policy_watermark, custom_domain, privacy_policy, metadata_user, custom_text_message, custom_text_login, lockout_policy, actions_allowed, max_actions) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)", expectedArgs: []interface{}{ "agg-id", anyArg{}, @@ -365,7 +368,8 @@ func TestFeatureProjection_reduces(t *testing.T) { true, true, true, - true, + domain.ActionsMaxAllowed, + 10, }, }, }, diff --git a/internal/repository/features/features.go b/internal/repository/features/features.go index 90dc60b32..fc045ab15 100644 --- a/internal/repository/features/features.go +++ b/internal/repository/features/features.go @@ -19,28 +19,30 @@ const ( type FeaturesSetEvent struct { eventstore.BaseEvent `json:"-"` - TierName *string `json:"tierName,omitempty"` - TierDescription *string `json:"tierDescription,omitempty"` - State *domain.FeaturesState `json:"state,omitempty"` - StateDescription *string `json:"stateDescription,omitempty"` - AuditLogRetention *time.Duration `json:"auditLogRetention,omitempty"` - LoginPolicyFactors *bool `json:"loginPolicyFactors,omitempty"` - LoginPolicyIDP *bool `json:"loginPolicyIDP,omitempty"` - LoginPolicyPasswordless *bool `json:"loginPolicyPasswordless,omitempty"` - LoginPolicyRegistration *bool `json:"loginPolicyRegistration,omitempty"` - LoginPolicyUsernameLogin *bool `json:"loginPolicyUsernameLogin,omitempty"` - LoginPolicyPasswordReset *bool `json:"loginPolicyPasswordReset,omitempty"` - PasswordComplexityPolicy *bool `json:"passwordComplexityPolicy,omitempty"` - LabelPolicy *bool `json:"labelPolicy,omitempty"` - LabelPolicyPrivateLabel *bool `json:"labelPolicyPrivateLabel,omitempty"` - LabelPolicyWatermark *bool `json:"labelPolicyWatermark,omitempty"` - CustomDomain *bool `json:"customDomain,omitempty"` - PrivacyPolicy *bool `json:"privacyPolicy,omitempty"` - MetadataUser *bool `json:"metadataUser,omitempty"` - CustomTextMessage *bool `json:"customTextMessage,omitempty"` - CustomTextLogin *bool `json:"customTextLogin,omitempty"` - LockoutPolicy *bool `json:"lockoutPolicy,omitempty"` - Actions *bool `json:"actions,omitempty"` + TierName *string `json:"tierName,omitempty"` + TierDescription *string `json:"tierDescription,omitempty"` + State *domain.FeaturesState `json:"state,omitempty"` + StateDescription *string `json:"stateDescription,omitempty"` + AuditLogRetention *time.Duration `json:"auditLogRetention,omitempty"` + LoginPolicyFactors *bool `json:"loginPolicyFactors,omitempty"` + LoginPolicyIDP *bool `json:"loginPolicyIDP,omitempty"` + LoginPolicyPasswordless *bool `json:"loginPolicyPasswordless,omitempty"` + LoginPolicyRegistration *bool `json:"loginPolicyRegistration,omitempty"` + LoginPolicyUsernameLogin *bool `json:"loginPolicyUsernameLogin,omitempty"` + LoginPolicyPasswordReset *bool `json:"loginPolicyPasswordReset,omitempty"` + PasswordComplexityPolicy *bool `json:"passwordComplexityPolicy,omitempty"` + LabelPolicy *bool `json:"labelPolicy,omitempty"` + LabelPolicyPrivateLabel *bool `json:"labelPolicyPrivateLabel,omitempty"` + LabelPolicyWatermark *bool `json:"labelPolicyWatermark,omitempty"` + CustomDomain *bool `json:"customDomain,omitempty"` + PrivacyPolicy *bool `json:"privacyPolicy,omitempty"` + MetadataUser *bool `json:"metadataUser,omitempty"` + CustomTextMessage *bool `json:"customTextMessage,omitempty"` + CustomTextLogin *bool `json:"customTextLogin,omitempty"` + LockoutPolicy *bool `json:"lockoutPolicy,omitempty"` + Actions *bool `json:"actions,omitempty"` + ActionsAllowed *domain.ActionsAllowed `json:"actionsAllowed,omitempty"` + MaxActions *int `json:"maxActions,omitempty"` } func (e *FeaturesSetEvent) Data() interface{} { @@ -189,9 +191,15 @@ func ChangeLockoutPolicy(lockoutPolicy bool) func(event *FeaturesSetEvent) { } } -func ChangeActions(actions bool) func(event *FeaturesSetEvent) { +func ChangeActionsAllowed(allowedType domain.ActionsAllowed) func(event *FeaturesSetEvent) { return func(e *FeaturesSetEvent) { - e.Actions = &actions + e.ActionsAllowed = &allowedType + } +} + +func ChangeMaxActions(maxActions int) func(event *FeaturesSetEvent) { + return func(e *FeaturesSetEvent) { + e.MaxActions = &maxActions } } diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index cd72d44a6..29f0e8efa 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -377,6 +377,7 @@ Errors: NotFound: Action wurde nicht gefunden NotActive: Action ist nicht aktiv NotInactive: Action ist nicht inaktiv + MaxAllowed: Keine weitern aktiven Actions mehr erlaubt Flow: FlowTypeMissing: FlowType fehlt Empty: Flow ist bereits leer diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 78aa331df..4753cc51d 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -377,6 +377,7 @@ Errors: NotFound: Action not found NotActive: Action is not active NotInactive: Action is not inactive + MaxAllowed: No additional active Actions allowed Flow: FlowTypeMissing: FlowType missing Empty: Flow is already empty diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 40c3aaead..16912e187 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -375,6 +375,7 @@ Errors: NotFound: L'azione non trovata NotActive: L'azione non è attiva NotInactive: L'azione non è inattiva + MaxAllowed: Non sono permesse altre azioni attive Flow: FlowTypeMissing: FlowType mancante Empty: Flow è già vuoto diff --git a/internal/ui/login/handler/custom_action.go b/internal/ui/login/handler/custom_action.go index 20c2241d5..3ff99b3b6 100644 --- a/internal/ui/login/handler/custom_action.go +++ b/internal/ui/login/handler/custom_action.go @@ -22,7 +22,7 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte } resourceOwner = iam.GlobalOrgID } - triggerActions, err := l.query.GetActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner) + triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner) if err != nil { return nil, err } @@ -38,7 +38,7 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte } func (l *Login) customExternalUserToLoginUserMapping(user *domain.Human, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, metadata []*domain.Metadata, resourceOwner string) (*domain.Human, []*domain.Metadata, error) { - triggerActions, err := l.query.GetActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePreCreation, resourceOwner) + triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePreCreation, resourceOwner) if err != nil { return nil, nil, err } @@ -54,7 +54,7 @@ func (l *Login) customExternalUserToLoginUserMapping(user *domain.Human, tokens } func (l *Login) customGrants(userID string, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, resourceOwner string) ([]*domain.UserGrant, error) { - triggerActions, err := l.query.GetActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePostCreation, resourceOwner) + triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePostCreation, resourceOwner) if err != nil { return nil, err } diff --git a/migrations/cockroach/V1.109__features_max_actions.sql b/migrations/cockroach/V1.109__features_max_actions.sql new file mode 100644 index 000000000..c66bed939 --- /dev/null +++ b/migrations/cockroach/V1.109__features_max_actions.sql @@ -0,0 +1,5 @@ +alter table zitadel.projections.features + ADD COLUMN actions_allowed INT2 default 0, + ADD COLUMN max_actions INT8 default 0; + +update zitadel.projections.features set actions_allowed = 2 where actions = true; diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 197022df4..8a0628c93 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2822,6 +2822,8 @@ message SetDefaultFeaturesRequest { bool custom_text_login = 21; bool lockout_policy = 22; bool actions = 23; + zitadel.features.v1.ActionsAllowed actions_allowed = 24; + int32 max_actions = 25; } message SetDefaultFeaturesResponse { @@ -2862,6 +2864,8 @@ message SetOrgFeaturesRequest { bool custom_text_login = 22; bool lockout_policy = 23; bool actions = 24; + zitadel.features.v1.ActionsAllowed actions_allowed = 25; + int32 max_actions = 26; } message SetOrgFeaturesResponse { diff --git a/proto/zitadel/features.proto b/proto/zitadel/features.proto index 660c8f7db..cc84241bd 100644 --- a/proto/zitadel/features.proto +++ b/proto/zitadel/features.proto @@ -31,6 +31,8 @@ message Features { bool custom_text_login = 20; bool lockout_policy = 21; bool actions = 22; + ActionsAllowed actions_allowed = 23; + int32 max_actions = 24; } message FeatureTier { @@ -47,3 +49,9 @@ enum FeaturesState { FEATURES_STATE_CANCELED = 2; FEATURES_STATE_GRANDFATHERED = 3; } + +enum ActionsAllowed { + ACTIONS_ALLOWED_NOT_ALLOWED = 0; + ACTIONS_ALLOWED_MAX = 1; + ACTIONS_ALLOWED_UNLIMITED = 2; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 385f181de..e454382cd 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2794,30 +2794,29 @@ service ManagementService { }; } -//TODO: enable in next release -// rpc DeactivateAction(DeactivateActionRequest) returns (DeactivateActionResponse) { -// option (google.api.http) = { -// post: "/actions/{id}/_deactivate" -// body: "*" -// }; -// -// option (zitadel.v1.auth_option) = { -// permission: "org.action.write" -// feature: "actions" -// }; -// } -// -// rpc ReactivateAction(ReactivateActionRequest) returns (ReactivateActionResponse) { -// option (google.api.http) = { -// post: "/actions/{id}/_reactivate" -// body: "*" -// }; -// -// option (zitadel.v1.auth_option) = { -// permission: "org.action.write" -// feature: "actions" -// }; -// } + rpc DeactivateAction(DeactivateActionRequest) returns (DeactivateActionResponse) { + option (google.api.http) = { + post: "/actions/{id}/_deactivate" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.write" + feature: "actions" + }; + } + + rpc ReactivateAction(ReactivateActionRequest) returns (ReactivateActionResponse) { + option (google.api.http) = { + post: "/actions/{id}/_reactivate" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.action.write" + feature: "actions" + }; + } rpc DeleteAction(DeleteActionRequest) returns (DeleteActionResponse) { option (google.api.http) = {