feat: limit amount of active actions (#3143)

* max actions

* fix: max allowed actions

* fix: max allowed actions

* fix tests
This commit is contained in:
Livio Amstutz
2022-02-02 09:04:05 +01:00
committed by GitHub
parent 585ebf9a81
commit 1367a2e139
32 changed files with 583 additions and 123 deletions
+12 -2
View File
@@ -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
+2 -1
View File
@@ -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,
}
}
+2 -1
View File
@@ -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")
+8 -4
View File
@@ -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
+58 -2
View File
@@ -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
}
+16 -3
View File
@@ -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()
}
+186 -9
View File
@@ -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(),
+30 -2
View File
@@ -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
}
+8 -4
View File
@@ -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 {
+6 -6
View File
@@ -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{