fix: actions v2 circular check for includes (#7563)

Add a check for circular includes in action v2 executions, so that no
self-includes or infinite loops can happen.

Closes #7445 

### Definition of Ready

- [x] I am happy with the code
- [x] Short description of the feature/issue is added in the pr
description
- [x] PR is linked to the corresponding user story
- [x] Acceptance criteria are met
- [x] All open todos and follow ups are defined in a new ticket and
justified
- [x] Deviations from the acceptance criteria and design are agreed with
the PO and documented.
- [x] No debug or dead code
- [x] My code has no repetitions
- [x] Critical parts are tested automatically
- [x] Where possible E2E tests are implemented
- [x] Documentation/examples are up-to-date
- [x] All non-functional requirements are met
- [x] Functionality of the acceptance criteria is checked manually on
the dev system.

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2024-05-22 18:05:06 +02:00
committed by GitHub
parent fb162a7d75
commit f37113194d
5 changed files with 624 additions and 25 deletions

View File

@@ -348,6 +348,16 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
"push ok, method include",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
),
),
),
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
@@ -419,6 +429,16 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
"push ok, service include",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
),
),
),
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
@@ -489,6 +509,16 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
"push ok, all include",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
),
),
),
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
@@ -2373,3 +2403,470 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) {
})
}
}
func mockExecutionIncludesCache(cache map[string][]string) includeCacheFunc {
return func(ctx context.Context, id string, resourceOwner string) ([]string, error) {
included, ok := cache[id]
if !ok {
return nil, zerrors.ThrowPreconditionFailed(nil, "", "cache failed")
}
return included, nil
}
}
func TestCommands_checkForIncludeCircular(t *testing.T) {
type args struct {
ctx context.Context
id string
resourceOwner string
includes []string
cache map[string][]string
}
type res struct {
err func(error) bool
}
tests := []struct {
name string
args args
res res
}{
{
"not found, error",
args{
ctx: context.Background(),
id: "id",
resourceOwner: "",
includes: []string{"notexistent"},
cache: map[string][]string{},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"single, ok",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id2"},
cache: map[string][]string{
"id2": {},
},
},
res{},
},
{
"single, circular",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id1"},
cache: map[string][]string{},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"multi 3, ok",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id2"},
cache: map[string][]string{
"id2": {"id3"},
"id3": {},
},
},
res{},
},
{
"multi 3, circular",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id2"},
cache: map[string][]string{
"id2": {"id3"},
"id3": {"id1"},
},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"multi 5, ok",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id11", "id12"},
cache: map[string][]string{
"id11": {"id21", "id23"},
"id12": {"id22"},
"id21": {},
"id22": {},
"id23": {},
},
},
res{},
},
{
"multi 5, circular",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id11", "id12"},
cache: map[string][]string{
"id11": {"id21", "id23"},
"id12": {"id22"},
"id21": {},
"id22": {},
"id23": {"id1"},
},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"multi 5, circular",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id11", "id12"},
cache: map[string][]string{
"id11": {"id21", "id23"},
"id12": {"id22"},
"id21": {},
"id22": {},
"id23": {"id11"},
},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"multi 5, circular",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id11", "id12"},
cache: map[string][]string{
"id11": {"id21", "id23"},
"id12": {"id22"},
"id21": {"id11"},
"id22": {},
"id23": {},
},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"multi 5, circular",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id11", "id12"},
cache: map[string][]string{
"id11": {"id21", "id23"},
"id12": {"id22"},
"id21": {},
"id22": {"id12"},
"id23": {},
},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"multi 3, maxlevel",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id2"},
cache: map[string][]string{
"id2": {"id3"},
"id3": {},
},
},
res{},
},
{
"multi 4, over maxlevel",
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "",
includes: []string{"id2"},
cache: map[string][]string{
"id2": {"id3"},
"id3": {"id4"},
"id4": {},
},
},
res{
err: zerrors.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := mockExecutionIncludesCache(tt.args.cache)
err := checkForIncludeCircular(tt.args.ctx, tt.args.id, tt.args.resourceOwner, tt.args.includes, f, 3)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func mockExecutionIncludesCacheFuncs(cache map[string][]string) (func(string) ([]string, bool), func(string, []string)) {
return func(s string) ([]string, bool) {
includes, ok := cache[s]
return includes, ok
}, func(s string, strings []string) {
cache[s] = strings
}
}
func TestCommands_getExecutionIncludes(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
cache map[string][]string
id string
resourceOwner string
}
type res struct {
includes []string
cache map[string][]string
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"new empty, ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
),
),
),
),
},
args{
ctx: context.Background(),
cache: map[string][]string{},
id: "id",
resourceOwner: "instance",
},
res{
includes: []string{},
cache: map[string][]string{"id": {}},
},
},
{
"new includes, ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
),
},
args{
ctx: context.Background(),
cache: map[string][]string{},
id: "id",
resourceOwner: "instance",
},
res{
includes: []string{"include"},
cache: map[string][]string{"id": {"include"}},
},
},
{
"found, ok",
fields{
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
cache: map[string][]string{"id": nil},
id: "id",
resourceOwner: "instance",
},
res{
includes: nil,
cache: map[string][]string{"id": nil},
},
},
{
"found includes, ok",
fields{
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
cache: map[string][]string{"id": {"include1", "include2", "include3"}},
id: "id",
resourceOwner: "instance",
},
res{
includes: []string{"include1", "include2", "include3"},
cache: map[string][]string{"id": {"include1", "include2", "include3"}},
},
},
{
"found multiple, ok",
fields{
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
cache: map[string][]string{
"id1": {"include1", "include2", "include3"},
"id2": {"include1", "include2", "include3"},
"id3": {"include1", "include2", "include3"},
},
id: "id2",
resourceOwner: "instance",
},
res{
includes: []string{"include1", "include2", "include3"},
cache: map[string][]string{
"id1": {"include1", "include2", "include3"},
"id2": {"include1", "include2", "include3"},
"id3": {"include1", "include2", "include3"},
},
},
},
{
"new multiple, ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
),
),
),
),
},
args{
ctx: context.Background(),
cache: map[string][]string{
"id1": {"include1", "include2", "include3"},
"id2": {"include1", "include2", "include3"},
"id3": {"include1", "include2", "include3"},
},
id: "id",
resourceOwner: "instance",
},
res{
includes: []string{},
cache: map[string][]string{
"id1": {"include1", "include2", "include3"},
"id2": {"include1", "include2", "include3"},
"id3": {"include1", "include2", "include3"},
"id": {},
},
},
},
{
"new multiple includes, ok",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("request/include", "instance"),
[]*execution.Target{
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
),
},
args{
ctx: context.Background(),
cache: map[string][]string{
"id1": {"include1", "include2", "include3"},
"id2": {"include1", "include2", "include3"},
"id3": {"include1", "include2", "include3"},
},
id: "id",
resourceOwner: "instance",
},
res{
includes: []string{"include"},
cache: map[string][]string{
"id1": {"include1", "include2", "include3"},
"id2": {"include1", "include2", "include3"},
"id3": {"include1", "include2", "include3"},
"id": {"include"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
includes, err := c.getExecutionIncludes(mockExecutionIncludesCacheFuncs(tt.args.cache))(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.cache, tt.args.cache)
assert.Equal(t, tt.res.includes, includes)
}
})
}
}