Stefan Benz b8ba7bd5ba
fix: remove action feature flag and include execution (#9727)
# Which Problems Are Solved

Actions v2 is not a feature flag anymore, include functionality on
executions is not used and json tags of proto messages are handled
incorrectly.

# How the Problems Are Solved

- Remove actions from the feature flags on system and instance level
- Remove include type on executions, only in the API, later maybe in the
handling logic as well
- Use protojson in request and response handling of actions v2

# Additional Changes

- Correct integration tests for request and response handling
- Use json.RawMessage for events, so that the event payload is not
base64 encoded
- Added separate context for async webhook calls, that executions are
not cancelled when called async

# Additional Context

Related to #9759 
Closes #9710

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
2025-04-28 11:24:50 +02:00

566 lines
14 KiB
Go

//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
)
func TestServer_SetExecution_Request(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "service, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "NotExistingService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *action.SetExecutionResponse) {
if expectedSetDate {
if !setDate.IsZero() {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate)
} else {
assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.SetDate)
}
}
func TestServer_SetExecution_Response(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "service, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "NotExistingService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{
All: true,
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Event(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "event, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "user.human.notexisting",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "event, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "user.human.added",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "group, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user.notexisting",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "group, level 1, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "group, level 2, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "user.human",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Function(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
wantSetDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "function, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
{
name: "function, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "presamlresponse"},
},
},
Targets: []string{targetResp.GetId()},
},
wantSetDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2beta.SetExecution(tt.ctx, tt.req)
setDate := time.Now().UTC()
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}