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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 583 additions and 123 deletions

View File

@ -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 | - | |

View File

@ -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))

View File

@ -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),
}
}

View File

@ -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
}
}

View File

@ -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

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

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,
}
}

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")

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

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
}

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()
}

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(),

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
}

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 {

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{

View File

@ -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
)

View File

@ -54,7 +54,8 @@ type Features struct {
PrivacyPolicy bool
MetadataUser bool
LockoutPolicy bool
Actions bool
ActionsAllowed ActionsAllowed
MaxActions int
}
type FeaturesState int32

View File

@ -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,

View File

@ -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`+

View File

@ -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

View File

@ -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,
),

View File

@ -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,

View File

@ -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,
},
},
},

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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;

View File

@ -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 {

View File

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

View File

@ -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) = {