feat(v3alpha): write actions (#8225)

# Which Problems Are Solved

The current v3alpha actions APIs don't exactly adhere to the [new
resources API
design](https://zitadel.com/docs/apis/v3#standard-resources).

# How the Problems Are Solved

- **Breaking**: The current v3alpha actions APIs are removed. This is
breaking.
- **Resource Namespace**: New v3alpha actions APIs for targets and
executions are added under the namespace /resources.
- **Feature Flag**: New v3alpha actions APIs still have to be activated
using the actions feature flag
- **Reduced Executions Overhead**: Executions are managed similar to
settings according to the new API design: an empty list of targets
basically makes an execution a Noop. So a single method, SetExecution is
enough to cover all use cases. Noop executions are not returned in
future search requests.
- **Compatibility**: The executions created with previous v3alpha APIs
are still available to be managed with the new executions API.

# Additional Changes

- Removed integration tests which test executions but rely on readable
targets. They are added again with #8169

# Additional Context

Closes #8168
This commit is contained in:
Elio Bischof
2024-07-31 14:42:12 +02:00
committed by GitHub
parent a1d24353db
commit cc3ec1e2a7
50 changed files with 2822 additions and 5570 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,323 +0,0 @@
//go:build integration
package action_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
func TestServer_ExecutionTarget(t *testing.T) {
ensureFeatureEnabled(t)
fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID"
tests := []struct {
name string
ctx context.Context
dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) (func(), error)
clean func(context.Context)
req *action.GetTargetByIDRequest
want *action.GetTargetByIDResponse
wantErr bool
}{
{
name: "GetTargetByID, request and response, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) {
instanceID := Tester.Instance.InstanceID()
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID
// create target for target changes
targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1)
targetCreatedURL := "https://nonexistent"
targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false)
// request received by target
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request}
changedRequest := &action.GetTargetByIDRequest{TargetId: targetCreated.GetId()}
// replace original request with different targetID
urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest)
targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, false)
Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId()))
// GetTargetByID with used target
request.TargetId = targetRequest.GetId()
// expected response from the GetTargetByID
expectedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: targetCreated.GetId(),
Details: targetCreated.GetDetails(),
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
}
// has to be set separately because of the pointers
response.Target = &action.Target{
TargetId: targetCreated.GetId(),
Details: targetCreated.GetDetails(),
Name: targetCreatedName,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
}
// content for partial update
changedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: "changed",
},
}
// change partial updated content on returned response
response.Target.TargetId = changedResponse.Target.TargetId
// response received by target
wantResponse := &middleware.ContextInfoResponse{
FullMethod: fullMethod,
InstanceID: instanceID,
OrgID: orgID,
ProjectID: projectID,
UserID: userID,
Request: changedRequest,
Response: expectedResponse,
}
// after request with different targetID, return changed response
targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse)
targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, false)
Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId()))
return func() {
closeRequest()
closeResponse()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetByIDRequest{},
want: &action.GetTargetByIDResponse{},
},
/*{
name: "GetTargetByID, request, interrupt",
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) {
fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID"
instanceID := Tester.Instance.InstanceID()
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID
// request received by target
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request}
urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetByIDRequest{TargetId: "notchanged"})
targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, true)
Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId()))
// GetTargetByID with used target
request.TargetId = targetRequest.GetId()
return func() {
closeRequest()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
},
req: &action.GetTargetByIDRequest{},
wantErr: true,
},
{
name: "GetTargetByID, response, interrupt",
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) {
fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID"
instanceID := Tester.Instance.InstanceID()
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID
// create target for target changes
targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1)
targetCreatedURL := "https://nonexistent"
targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false)
// GetTargetByID with used target
request.TargetId = targetCreated.GetId()
// expected response from the GetTargetByID
expectedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: targetCreated.GetId(),
Details: targetCreated.GetDetails(),
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
}
// content for partial update
changedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: "changed",
},
}
// response received by target
wantResponse := &middleware.ContextInfoResponse{
FullMethod: fullMethod,
InstanceID: instanceID,
OrgID: orgID,
ProjectID: projectID,
UserID: userID,
Request: request,
Response: expectedResponse,
}
// after request with different targetID, return changed response
targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse)
targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, true)
Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId()))
return func() {
closeResponse()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetByIDRequest{},
wantErr: true,
},*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
close, err := tt.dep(tt.ctx, tt.req, tt.want)
require.NoError(t, err)
defer close()
}
got, err := Client.GetTargetByID(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget())
assert.Equal(t, tt.want.Target.TargetId, got.Target.TargetId)
if tt.clean != nil {
tt.clean(tt.ctx)
}
})
}
}
func conditionRequestFullMethod(fullMethod string) *action.Condition {
return &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: fullMethod,
},
},
},
}
}
func conditionResponseFullMethod(fullMethod string) *action.Condition {
return &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: fullMethod,
},
},
},
}
}
func testServerCall(
reqBody interface{},
sleep time.Duration,
statusCode int,
respBody interface{},
) (string, func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(reqBody)
if err != nil {
http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError)
return
}
sentBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError)
return
}
if !reflect.DeepEqual(data, sentBody) {
http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError)
return
}
if statusCode != http.StatusOK {
http.Error(w, "error, statusCode", statusCode)
return
}
time.Sleep(sleep)
w.Header().Set("Content-Type", "application/json")
resp, err := json.Marshal(respBody)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if _, err := io.WriteString(w, string(resp)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
return server.URL, server.Close
}

View File

@@ -1,364 +0,0 @@
package action
import (
"context"
"strings"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
queries, err := listTargetsRequestToModel(req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchTargets(ctx, queries)
if err != nil {
return nil, err
}
return &action.ListTargetsResponse{
Result: targetsToPb(resp.Targets),
Details: object.ToListDetails(resp.SearchResponse),
}, nil
}
func listTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := targetQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
return &query.TargetSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func targetFieldNameToSortingColumn(field action.TargetFieldName) query.Column {
switch field {
case action.TargetFieldName_FIELD_NAME_UNSPECIFIED:
return query.TargetColumnID
case action.TargetFieldName_FIELD_NAME_ID:
return query.TargetColumnID
case action.TargetFieldName_FIELD_NAME_CREATION_DATE:
return query.TargetColumnCreationDate
case action.TargetFieldName_FIELD_NAME_CHANGE_DATE:
return query.TargetColumnChangeDate
case action.TargetFieldName_FIELD_NAME_NAME:
return query.TargetColumnName
case action.TargetFieldName_FIELD_NAME_TARGET_TYPE:
return query.TargetColumnTargetType
case action.TargetFieldName_FIELD_NAME_URL:
return query.TargetColumnURL
case action.TargetFieldName_FIELD_NAME_TIMEOUT:
return query.TargetColumnTimeout
case action.TargetFieldName_FIELD_NAME_INTERRUPT_ON_ERROR:
return query.TargetColumnInterruptOnError
default:
return query.TargetColumnID
}
}
func targetQueriesToQuery(queries []*action.TargetSearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = targetQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func targetQueryToQuery(query *action.TargetSearchQuery) (query.SearchQuery, error) {
switch q := query.Query.(type) {
case *action.TargetSearchQuery_TargetNameQuery:
return targetNameQueryToQuery(q.TargetNameQuery)
case *action.TargetSearchQuery_InTargetIdsQuery:
return targetInTargetIdsQueryToQuery(q.InTargetIdsQuery)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func targetNameQueryToQuery(q *action.TargetNameQuery) (query.SearchQuery, error) {
return query.NewTargetNameSearchQuery(object.TextMethodToQuery(q.Method), q.GetTargetName())
}
func targetInTargetIdsQueryToQuery(q *action.InTargetIDsQuery) (query.SearchQuery, error) {
return query.NewTargetInIDsSearchQuery(q.GetTargetIds())
}
func (s *Server) GetTargetByID(ctx context.Context, req *action.GetTargetByIDRequest) (_ *action.GetTargetByIDResponse, err error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
resp, err := s.query.GetTargetByID(ctx, req.GetTargetId())
if err != nil {
return nil, err
}
return &action.GetTargetByIDResponse{
Target: targetToPb(resp),
}, nil
}
func targetsToPb(targets []*query.Target) []*action.Target {
t := make([]*action.Target, len(targets))
for i, target := range targets {
t[i] = targetToPb(target)
}
return t
}
func targetToPb(t *query.Target) *action.Target {
target := &action.Target{
Details: object.DomainToDetailsPb(&t.ObjectDetails),
TargetId: t.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeCall:
target.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeAsync:
target.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}}
default:
target.TargetType = nil
}
return target
}
func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
queries, err := listExecutionsRequestToModel(req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchExecutions(ctx, queries)
if err != nil {
return nil, err
}
return &action.ListExecutionsResponse{
Result: executionsToPb(resp.Executions),
Details: object.ToListDetails(resp.SearchResponse),
}, nil
}
func listExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := executionQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
return &query.ExecutionSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func executionQueriesToQuery(queries []*action.SearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = executionQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func executionQueryToQuery(searchQuery *action.SearchQuery) (query.SearchQuery, error) {
switch q := searchQuery.Query.(type) {
case *action.SearchQuery_InConditionsQuery:
return inConditionsQueryToQuery(q.InConditionsQuery)
case *action.SearchQuery_ExecutionTypeQuery:
return executionTypeToQuery(q.ExecutionTypeQuery)
case *action.SearchQuery_IncludeQuery:
include, err := conditionToInclude(q.IncludeQuery.GetInclude())
if err != nil {
return nil, err
}
return query.NewIncludeSearchQuery(include)
case *action.SearchQuery_TargetQuery:
return query.NewTargetSearchQuery(q.TargetQuery.GetTargetId())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionTypeToQuery(q *action.ExecutionTypeQuery) (query.SearchQuery, error) {
switch q.ExecutionType {
case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
case action.ExecutionType_EXECUTION_TYPE_REQUEST:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest)
case action.ExecutionType_EXECUTION_TYPE_RESPONSE:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse)
case action.ExecutionType_EXECUTION_TYPE_EVENT:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent)
case action.ExecutionType_EXECUTION_TYPE_FUNCTION:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction)
default:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
}
}
func inConditionsQueryToQuery(q *action.InConditionsQuery) (query.SearchQuery, error) {
values := make([]string, len(q.GetConditions()))
for i, condition := range q.GetConditions() {
id, err := conditionToID(condition)
if err != nil {
return nil, err
}
values[i] = id
}
return query.NewExecutionInIDsSearchQuery(values)
}
func conditionToID(q *action.Condition) (string, error) {
switch t := q.GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
return cond.ID(domain.ExecutionTypeResponse), nil
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
return cond.ID(), nil
case *action.Condition_Function:
return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionsToPb(executions []*query.Execution) []*action.Execution {
e := make([]*action.Execution, len(executions))
for i, execution := range executions {
e[i] = executionToPb(execution)
}
return e
}
func executionToPb(e *query.Execution) *action.Execution {
targets := make([]*action.ExecutionTargetType, len(e.Targets))
for i := range e.Targets {
switch e.Targets[i].Type {
case domain.ExecutionTargetTypeInclude:
targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}}
case domain.ExecutionTargetTypeTarget:
targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}}
case domain.ExecutionTargetTypeUnspecified:
continue
default:
continue
}
}
return &action.Execution{
Details: object.DomainToDetailsPb(&e.ObjectDetails),
Condition: executionIDToCondition(e.ID),
Targets: targets,
}
}
func executionIDToCondition(include string) *action.Condition {
if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) {
return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) {
return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) {
return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) {
return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String()))
}
return nil
}
func includeRequestToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 2:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}}
case 1:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case 0:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}
default:
return nil
}
}
func includeResponseToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 2:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}}
case 1:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case 0:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}
default:
return nil
}
}
func includeEventToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 1:
if strings.HasSuffix(id, command.EventGroupSuffix) {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}}
} else {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}}
}
case 0:
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}
default:
return nil
}
}
func includeFunctionToCondition(id string) *action.Condition {
return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}}
}

View File

@@ -1,877 +0,0 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestServer_GetTargetByID(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) error
req *action.GetTargetByIDRequest
}
tests := []struct {
name string
args args
want *action.GetTargetByIDResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.GetTargetByIDRequest{},
},
wantErr: true,
},
{
name: "not found",
args: args{
ctx: CTX,
req: &action.GetTargetByIDRequest{TargetId: "notexisting"},
},
wantErr: true,
},
{
name: "get, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, async, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, webhook interruptOnError, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, call, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, call interruptOnError, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, getErr := Client.GetTargetByID(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, getErr, "Error: "+getErr.Error())
} else {
assert.NoError(ttt, getErr)
integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget())
assert.Equal(t, tt.want.Target, got.Target)
}
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func TestServer_ListTargets(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) error
req *action.ListTargetsRequest
}
tests := []struct {
name string
args args
want *action.ListTargetsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.ListTargetsRequest{},
},
wantErr: true,
},
{
name: "list, not found",
args: args{
ctx: CTX,
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{
{Query: &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{"notfound"},
},
},
},
},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 0,
},
Result: []*action.Target{},
},
},
{
name: "list single id",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{resp.GetId()},
},
}
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
//response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].TargetId = resp.GetId()
response.Result[0].Name = name
return nil
},
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{{}},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Target{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
}, {
name: "list single name",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Queries[0].Query = &action.TargetSearchQuery_TargetNameQuery{
TargetNameQuery: &action.TargetNameQuery{
TargetName: name,
},
}
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].TargetId = resp.GetId()
response.Result[0].Name = name
return nil
},
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{{}},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Target{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
{
name: "list multiple id",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name1 := fmt.Sprint(time.Now().UnixNano() + 1)
name2 := fmt.Sprint(time.Now().UnixNano() + 3)
name3 := fmt.Sprint(time.Now().UnixNano() + 5)
resp1 := Tester.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false)
resp2 := Tester.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true)
resp3 := Tester.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false)
request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
},
}
response.Details.Timestamp = resp3.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp3.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence()
response.Result[0].TargetId = resp1.GetId()
response.Result[0].Name = name1
response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence()
response.Result[1].TargetId = resp2.GetId()
response.Result[1].Name = name2
response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate()
response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence()
response.Result[2].TargetId = resp3.GetId()
response.Result[2].Name = name3
return nil
},
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{{}},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 3,
},
Result: []*action.Target{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListTargets(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(ttt, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func TestServer_ListExecutions(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) error
req *action.ListExecutionsRequest
}
tests := []struct {
name string
args args
want *action.ListExecutionsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.ListExecutionsRequest{},
},
wantErr: true,
},
{
name: "list request single condition",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
cond := request.Queries[0].GetInConditionsQuery().GetConditions()[0]
resp := Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId()))
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
// response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
// Set expected response with used values for SetExecution
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].Condition = cond
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_InConditionsQuery{
InConditionsQuery: &action.InConditionsQuery{
Conditions: []*action.Condition{{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
}},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
},
},
},
{
name: "list request single target",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
// add target as query to the request
request.Queries[0] = &action.SearchQuery{
Query: &action.SearchQuery_TargetQuery{
TargetQuery: &action.TargetQuery{
TargetId: target.GetId(),
},
},
}
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/UpdateAction",
},
},
},
}
targets := executionTargetsSingleTarget(target.GetId())
resp := Tester.SetExecution(ctx, t, cond, targets)
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].Condition = cond
response.Result[0].Targets = targets
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Condition: &action.Condition{},
Targets: executionTargetsSingleTarget(""),
},
},
},
}, {
name: "list request single include",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/GetAction",
},
},
},
}
Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId()))
request.Queries[0].GetIncludeQuery().Include = cond
includeCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/ListActions",
},
},
},
}
includeTargets := executionTargetsSingleInclude(cond)
resp2 := Tester.SetExecution(ctx, t, includeCond, includeTargets)
response.Details.Timestamp = resp2.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp2.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp2.GetDetails().GetSequence()
response.Result[0].Condition = includeCond
response.Result[0].Targets = includeTargets
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_IncludeQuery{
IncludeQuery: &action.IncludeQuery{},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
},
},
{
name: "list multiple conditions",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
cond1 := request.Queries[0].GetInConditionsQuery().GetConditions()[0]
targets1 := executionTargetsSingleTarget(targetResp.GetId())
resp1 := Tester.SetExecution(ctx, t, cond1, targets1)
response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence()
response.Result[0].Condition = cond1
response.Result[0].Targets = targets1
cond2 := request.Queries[0].GetInConditionsQuery().GetConditions()[1]
targets2 := executionTargetsSingleTarget(targetResp.GetId())
resp2 := Tester.SetExecution(ctx, t, cond2, targets2)
response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence()
response.Result[1].Condition = cond2
response.Result[1].Targets = targets2
cond3 := request.Queries[0].GetInConditionsQuery().GetConditions()[2]
targets3 := executionTargetsSingleTarget(targetResp.GetId())
resp3 := Tester.SetExecution(ctx, t, cond3, targets3)
response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate()
response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence()
response.Result[2].Condition = cond3
response.Result[2].Targets = targets3
response.Details.Timestamp = resp3.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp3.GetDetails().GetSequence()
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_InConditionsQuery{
InConditionsQuery: &action.InConditionsQuery{
Conditions: []*action.Condition{
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
},
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/CreateSession",
},
},
},
},
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/SetSession",
},
},
},
},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 3,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
}, {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
}, {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
},
},
{
name: "list multiple conditions all types",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
targets := executionTargetsSingleTarget(targetResp.GetId())
for i, cond := range request.Queries[0].GetInConditionsQuery().GetConditions() {
resp := Tester.SetExecution(ctx, t, cond, targets)
response.Result[i].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[i].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[i].Condition = cond
response.Result[i].Targets = targets
// filled with info of last sequence
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
}
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_InConditionsQuery{
InConditionsQuery: &action.InConditionsQuery{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 10,
},
Result: []*action.Execution{
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(t, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(t, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
// as not sorted, all elements have to be checked
// workaround as oneof elements can only be checked with assert.EqualExportedValues()
if j, found := containExecution(got.Result, tt.want.Result[i]); found {
assert.EqualExportedValues(t, tt.want.Result[i], got.Result[j])
}
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func containExecution(executionList []*action.Execution, execution *action.Execution) (int, bool) {
for i, exec := range executionList {
if reflect.DeepEqual(exec.Details, execution.Details) {
return i, true
}
}
return 0, false
}

View File

@@ -1,112 +0,0 @@
package action
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
add := createTargetToCommand(req)
details, err := s.command.AddTarget(ctx, add, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &action.CreateTargetResponse{
Id: add.AggregateID,
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.ChangeTarget(ctx, updateTargetToCommand(req), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &action.UpdateTargetResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteTarget(ctx, req.GetTargetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &action.DeleteTargetResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := req.GetTargetType().(type) {
case *action.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.CreateTargetRequest_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.CreateTargetRequest_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: req.GetName(),
TargetType: targetType,
Endpoint: req.GetEndpoint(),
Timeout: req.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget {
if req == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetTargetId(),
},
Name: req.Name,
Endpoint: req.Endpoint,
}
if req.TargetType != nil {
switch t := req.GetTargetType().(type) {
case *action.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.UpdateTargetRequest_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.UpdateTargetRequest_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if req.Timeout != nil {
target.Timeout = gu.Ptr(req.GetTimeout().AsDuration())
}
return target
}

View File

@@ -1,423 +0,0 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
)
func TestServer_CreateTarget(t *testing.T) {
ensureFeatureEnabled(t)
tests := []struct {
name string
ctx context.Context
req *action.CreateTargetRequest
want *action.CreateTargetResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "webhook, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "webhook, interrupt on error, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, interruptOnError, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
assert.NotEmpty(t, got.GetId())
})
}
}
func TestServer_UpdateTarget(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
req *action.UpdateTargetRequest
}
tests := []struct {
name string
prepare func(request *action.UpdateTargetRequest) error
args args
want *action.UpdateTargetResponse
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.UpdateTargetRequest) error {
request.TargetId = "notexisting"
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Timeout: durationpb.New(20 * time.Second),
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type async, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.UpdateTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.DeleteTargetRequest
want *action.DeleteTargetResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.DeleteTargetRequest{
TargetId: target.GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: CTX,
req: &action.DeleteTargetRequest{
TargetId: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: CTX,
req: &action.DeleteTargetRequest{
TargetId: target.GetId(),
},
want: &action.DeleteTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -4,38 +4,22 @@ import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
settings_object "github.com/zitadel/zitadel/internal/api/grpc/settings/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func (s *Server) ListExecutionFunctions(_ context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) {
return &action.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}, nil
}
func (s *Server) ListExecutionMethods(_ context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) {
return &action.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}, nil
}
func (s *Server) ListExecutionServices(_ context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) {
return &action.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}, nil
}
func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
targets := make([]*execution.Target, len(req.Targets))
for i, target := range req.Targets {
reqTargets := req.GetExecution().GetTargets()
targets := make([]*execution.Target, len(reqTargets))
for i, target := range reqTargets {
switch t := target.GetType().(type) {
case *action.ExecutionTargetType_Include:
include, err := conditionToInclude(t.Include)
@@ -50,36 +34,32 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
set := &command.SetExecution{
Targets: targets,
}
owner := &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: authz.GetInstance(ctx).InstanceID(),
}
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
details, err = s.command.SetExecutionRequest(ctx, cond, set, owner.Id)
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
details, err = s.command.SetExecutionResponse(ctx, cond, set, owner.Id)
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
details, err = s.command.SetExecutionEvent(ctx, cond, set, owner.Id)
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, owner.Id)
default:
err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid")
}
if err != nil {
return nil, err
}
return &action.SetExecutionResponse{
Details: object.DomainToDetailsPb(details),
Details: settings_object.DomainToDetailsPb(details, owner),
}, nil
}
@@ -109,44 +89,26 @@ func conditionToInclude(cond *action.Condition) (string, error) {
return "", err
}
return cond.ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid")
}
return "", nil
}
func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutionRequest) (*action.DeleteExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
func (s *Server) ListExecutionFunctions(_ context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) {
return &action.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}, nil
}
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.DeleteExecutionRequest(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.DeleteExecutionResponse(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.DeleteExecutionEvent(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Function:
details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
}
return &action.DeleteExecutionResponse{
Details: object.DomainToDetailsPb(details),
func (s *Server) ListExecutionMethods(_ context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) {
return &action.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}, nil
}
func (s *Server) ListExecutionServices(_ context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) {
return &action.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}, nil
}

View File

@@ -0,0 +1,805 @@
//go:build integration
package action_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha"
)
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}}
}
func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType {
return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}}
}
func TestServer_SetExecution_Request(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
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: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "service, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "NotExistingService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "service, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "all, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
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
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Request_Include(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
executionCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
}
Tester.SetExecution(CTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
)
circularExecutionService := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
}
Tester.SetExecution(CTX, t,
circularExecutionService,
executionTargetsSingleInclude(executionCond),
)
circularExecutionMethod := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
}
Tester.SetExecution(CTX, t,
circularExecutionMethod,
executionTargetsSingleInclude(circularExecutionService),
)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "method, circular error",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: circularExecutionService,
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(circularExecutionMethod),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "service, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
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
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Response(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
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: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "service, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "NotExistingService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "service, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "all, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
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
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Event(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
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: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
/*
//TODO event existing check
{
name: "event, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "event, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
/*
// TODO:
{
name: "group, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "group, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "all, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
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
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Function(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
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: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "function, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "function, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
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
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}

View File

@@ -10,13 +10,13 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var _ action.ActionServiceServer = (*Server)(nil)
var _ action.ZITADELActionsServer = (*Server)(nil)
type Server struct {
action.UnimplementedActionServiceServer
action.UnimplementedZITADELActionsServer
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
@@ -43,23 +43,23 @@ func CreateServer(
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
action.RegisterActionServiceServer(grpcServer, s)
action.RegisterZITADELActionsServer(grpcServer, s)
}
func (s *Server) AppName() string {
return action.ActionService_ServiceDesc.ServiceName
return action.ZITADELActions_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ActionService_ServiceDesc.ServiceName
return action.ZITADELActions_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ActionService_AuthMethods
return action.ZITADELActions_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterActionServiceHandler
return action.RegisterZITADELActionsHandler
}
func checkExecutionEnabled(ctx context.Context) error {

View File

@@ -13,14 +13,14 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var (
CTX context.Context
Tester *integration.Tester
Client action.ActionServiceClient
Client action.ZITADELActionsClient
)
func TestMain(m *testing.M) {

View File

@@ -0,0 +1,121 @@
package action
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
add := createTargetToCommand(req)
instance := targetOwnerInstance(ctx)
details, err := s.command.AddTarget(ctx, add, instance.Id)
if err != nil {
return nil, err
}
return &action.CreateTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, add.AggregateID),
}, nil
}
func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
instance := targetOwnerInstance(ctx)
details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instance.Id)
if err != nil {
return nil, err
}
return &action.PatchTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()),
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
instance := targetOwnerInstance(ctx)
details, err := s.command.DeleteTarget(ctx, req.GetId(), instance.Id)
if err != nil {
return nil, err
}
return &action.DeleteTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()),
}, nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
reqTarget := req.GetTarget()
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := reqTarget.GetTargetType().(type) {
case *action.Target_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.Target_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.Target_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: reqTarget.GetName(),
TargetType: targetType,
Endpoint: reqTarget.GetEndpoint(),
Timeout: reqTarget.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget {
reqTarget := req.GetTarget()
if reqTarget == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: reqTarget.Name,
Endpoint: reqTarget.Endpoint,
}
if reqTarget.TargetType != nil {
switch t := reqTarget.GetTargetType().(type) {
case *action.PatchTarget_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.PatchTarget_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.PatchTarget_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if reqTarget.Timeout != nil {
target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration())
}
return target
}
func targetOwnerInstance(ctx context.Context) *object.Owner {
return &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: authz.GetInstance(ctx).InstanceID(),
}
}

View File

@@ -0,0 +1,447 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_CreateTarget(t *testing.T) {
ensureFeatureEnabled(t)
tests := []struct {
name string
ctx context.Context
req *action.Target
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &action.Target{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "webhook, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "webhook, interrupt on error, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, interruptOnError, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
func TestServer_PatchTarget(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
req *action.PatchTargetRequest
}
tests := []struct {
name string
prepare func(request *action.PatchTargetRequest) error
args args
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.PatchTargetRequest) error {
request.Id = "notexisting"
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change url, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Timeout: durationpb.New(20 * time.Second),
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type async, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
// We want to have the same response no matter how often we call the function
Client.PatchTarget(tt.args.ctx, tt.args.req)
got, err := Client.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.DeleteTargetRequest
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: CTX,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: CTX,
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}

View File

@@ -10,12 +10,12 @@ import (
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.CreateTargetRequest
req *action.Target
}
tests := []struct {
name string
@@ -34,10 +34,10 @@ func Test_createTargetToCommand(t *testing.T) {
},
{
name: "all fields (webhook)",
args: args{&action.CreateTargetRequest{
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestWebhook{
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
@@ -52,10 +52,10 @@ func Test_createTargetToCommand(t *testing.T) {
},
{
name: "all fields (async)",
args: args{&action.CreateTargetRequest{
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestAsync{
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
@@ -70,10 +70,10 @@ func Test_createTargetToCommand(t *testing.T) {
},
{
name: "all fields (interrupting response)",
args: args{&action.CreateTargetRequest{
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestCall{
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
@@ -91,7 +91,7 @@ func Test_createTargetToCommand(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(tt.args.req)
got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req})
assert.Equal(t, tt.want, got)
})
}
@@ -99,7 +99,7 @@ func Test_createTargetToCommand(t *testing.T) {
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.UpdateTargetRequest
req *action.PatchTarget
}
tests := []struct {
name string
@@ -113,7 +113,7 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields nil",
args: args{&action.UpdateTargetRequest{
args: args{&action.PatchTarget{
Name: nil,
TargetType: nil,
Timeout: nil,
@@ -128,7 +128,7 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields empty",
args: args{&action.UpdateTargetRequest{
args: args{&action.PatchTarget{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
@@ -143,10 +143,10 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (webhook)",
args: args{&action.UpdateTargetRequest{
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
@@ -163,10 +163,10 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (webhook interrupt)",
args: args{&action.UpdateTargetRequest{
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
@@ -183,10 +183,10 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (async)",
args: args{&action.UpdateTargetRequest{
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestAsync{
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
@@ -201,10 +201,10 @@ func Test_updateTargetToCommand(t *testing.T) {
},
{
name: "all fields (interrupting response)",
args: args{&action.UpdateTargetRequest{
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestCall{
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
@@ -222,7 +222,7 @@ func Test_updateTargetToCommand(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateTargetToCommand(tt.args.req)
got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req})
assert.Equal(t, tt.want, got)
})
}

View File

@@ -0,0 +1,21 @@
package object
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner, id string) *resources_object.Details {
details := &resources_object.Details{
Id: id,
Sequence: objectDetail.Sequence,
Owner: owner,
}
if !objectDetail.EventDate.IsZero() {
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
}
return details
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/query"
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor {
@@ -143,6 +144,9 @@ func (c *ContextInfoRequest) GetHTTPRequestBody() []byte {
}
func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error {
if !json.Valid(resp) {
return zerrors.ThrowPreconditionFailed(nil, "ACTION-4m9s2", "Errors.Execution.ResponseIsNotValidJSON")
}
return json.Unmarshal(resp, c.Request)
}

View File

@@ -0,0 +1,20 @@
package object
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner) *settings_object.Details {
details := &settings_object.Details{
Sequence: objectDetail.Sequence,
Owner: owner,
}
if !objectDetail.EventDate.IsZero() {
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
}
return details
}

View File

@@ -60,6 +60,11 @@ func (c *Commands) SetExecutionRequest(ctx context.Context, cond *ExecutionAPICo
if err := cond.IsValid(); err != nil {
return nil, err
}
for _, target := range set.Targets {
if err = target.Validate(); err != nil {
return nil, err
}
}
if err := cond.Existing(c); err != nil {
return nil, err
}
@@ -73,6 +78,11 @@ func (c *Commands) SetExecutionResponse(ctx context.Context, cond *ExecutionAPIC
if err := cond.IsValid(); err != nil {
return nil, err
}
for _, target := range set.Targets {
if err = target.Validate(); err != nil {
return nil, err
}
}
if err := cond.Existing(c); err != nil {
return nil, err
}
@@ -106,9 +116,19 @@ func (c *Commands) SetExecutionFunction(ctx context.Context, cond ExecutionFunct
if err := cond.IsValid(); err != nil {
return nil, err
}
for _, target := range set.Targets {
if err = target.Validate(); err != nil {
return nil, err
}
}
if err := cond.Existing(c); err != nil {
return nil, err
}
for _, target := range set.Targets {
if err = target.Validate(); err != nil {
return nil, err
}
}
if set.AggregateID == "" {
set.AggregateID = cond.ID()
}
@@ -165,6 +185,11 @@ func (c *Commands) SetExecutionEvent(ctx context.Context, cond *ExecutionEventCo
if err := cond.IsValid(); err != nil {
return nil, err
}
for _, target := range set.Targets {
if err = target.Validate(); err != nil {
return nil, err
}
}
if err := cond.Existing(c); err != nil {
return nil, err
}
@@ -200,13 +225,6 @@ func (t SetExecution) GetTargets() []string {
return targets
}
func (e *SetExecution) IsValid() error {
if len(e.Targets) == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-56bteot2uj", "Errors.Execution.NoTargets")
}
return nil
}
func (e *SetExecution) Existing(c *Commands, ctx context.Context, resourceOwner string) error {
targets := e.GetTargets()
if len(targets) > 0 && !c.existsTargetsByIDs(ctx, targets, resourceOwner) {
@@ -225,16 +243,17 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource
if resourceOwner == "" || set.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-gg3a6ol4om", "Errors.IDMissing")
}
if err := set.IsValid(); err != nil {
wm, err := c.getExecutionWriteModelByID(ctx, set.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
wm := NewExecutionWriteModel(set.AggregateID, resourceOwner)
// Check if targets and includes for execution are existing
if wm.ExecutionTargetsEqual(set.Targets) {
return writeModelToObjectDetails(&wm.WriteModel), err
}
if err := set.Existing(c, ctx, resourceOwner); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEventV2(
ctx,
ExecutionAggregateFromWriteModel(&wm.WriteModel),
@@ -245,55 +264,6 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) DeleteExecutionRequest(ctx context.Context, cond *ExecutionAPICondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(domain.ExecutionTypeRequest), resourceOwner)
}
func (c *Commands) DeleteExecutionResponse(ctx context.Context, cond *ExecutionAPICondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(domain.ExecutionTypeResponse), resourceOwner)
}
func (c *Commands) DeleteExecutionFunction(ctx context.Context, cond ExecutionFunctionCondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(), resourceOwner)
}
func (c *Commands) DeleteExecutionEvent(ctx context.Context, cond *ExecutionEventCondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(), resourceOwner)
}
func (c *Commands) deleteExecution(ctx context.Context, aggID string, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if resourceOwner == "" || aggID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-cnic97c0g3", "Errors.IDMissing")
}
wm, err := c.getExecutionWriteModelByID(ctx, aggID, resourceOwner)
if err != nil {
return nil, err
}
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-suq2upd3rt", "Errors.Execution.NotFound")
}
if err := c.pushAppendAndReduce(ctx, wm, execution.NewRemovedEvent(
ctx,
ExecutionAggregateFromWriteModel(&wm.WriteModel),
)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) existsExecutionsByIDs(ctx context.Context, ids []string, resourceOwner string) bool {
wm := NewExecutionsExistWriteModel(ids, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)

View File

@@ -16,6 +16,18 @@ type ExecutionWriteModel struct {
ExecutionTargets []*execution.Target
}
func (e *ExecutionWriteModel) ExecutionTargetsEqual(targets []*execution.Target) bool {
if len(e.ExecutionTargets) != len(targets) {
return false
}
for i := range e.ExecutionTargets {
if e.ExecutionTargets[i].Type != targets[i].Type || e.ExecutionTargets[i].Target != targets[i].Target {
return false
}
}
return true
}
func (e *ExecutionWriteModel) IncludeList() []string {
includes := make([]string, 0)
for i := range e.ExecutionTargets {

File diff suppressed because it is too large Load Diff

View File

@@ -46,7 +46,7 @@ func CallTargets(
}
if len(resp) > 0 {
// error in unmarshalling
if err := info.SetHTTPResponseBody(resp); err != nil {
if err := info.SetHTTPResponseBody(resp); err != nil && target.IsInterruptOnError() {
return nil, err
}
}
@@ -73,10 +73,10 @@ func CallTarget(
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
// get request, return response and error
case domain.TargetTypeCall:
return call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
case domain.TargetTypeAsync:
go func(target Target, info ContextInfoRequest) {
if _, err := call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
}
}(target, info)
@@ -88,12 +88,12 @@ func CallTarget(
// webhook call a webhook, ignore the response but return the errror
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error {
_, err := call(ctx, url, timeout, body)
_, err := Call(ctx, url, timeout, body)
return err
}
// call function to do a post HTTP request to a desired url with timeout
func call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) {
// Call function to do a post HTTP request to a desired url with timeout
func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, span := tracing.NewSpan(ctx)
defer func() {

View File

@@ -1,8 +1,8 @@
package execution
package execution_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
@@ -12,37 +12,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
)
var _ Target = &mockTarget{}
type mockTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *mockTarget) GetTargetID() string {
return e.TargetID
}
func (e *mockTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *mockTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *mockTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *mockTarget) GetTimeout() time.Duration {
return e.Timeout
}
func Test_Call(t *testing.T) {
type args struct {
ctx context.Context
@@ -110,12 +84,14 @@ func Test_Call(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
respBody, err := testServerCall(t,
tt.args.method,
tt.args.body,
tt.args.sleep,
tt.args.statusCode,
tt.args.respBody,
respBody, err := testServer(t,
&callTestServer{
method: tt.args.method,
expectBody: tt.args.body,
timeout: tt.args.sleep,
statusCode: tt.args.statusCode,
respondBody: tt.args.respBody,
},
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
)
if tt.res.wantErr {
@@ -129,98 +105,12 @@ func Test_Call(t *testing.T) {
}
}
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
return func(url string) ([]byte, error) {
return call(ctx, url, timeout, body)
}
}
func testCallTarget(ctx context.Context,
target *mockTarget,
info ContextInfoRequest,
) func(string) ([]byte, error) {
return func(url string) (r []byte, err error) {
target.Endpoint = url
return CallTarget(ctx, target, info)
}
}
func testServerCall(
t *testing.T,
method string,
body []byte,
timeout time.Duration,
statusCode int,
respBody []byte,
call func(string) ([]byte, error),
) ([]byte, error) {
handler := func(w http.ResponseWriter, r *http.Request) {
checkRequest(t, r, method, body)
if statusCode != http.StatusOK {
http.Error(w, "error", statusCode)
return
}
time.Sleep(timeout)
w.Header().Set("Content-Type", "application/json")
if _, err := io.WriteString(w, string(respBody)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
return call(server.URL)
}
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
sentBody, err := io.ReadAll(sent.Body)
require.NoError(t, err)
require.Equal(t, expectedBody, sentBody)
require.Equal(t, method, sent.Method)
}
var _ ContextInfoRequest = &mockContextInfoRequest{}
type request struct {
Request string `json:"request"`
}
type mockContextInfoRequest struct {
Request *request `json:"request"`
}
func newMockContextInfoRequest(s string) *mockContextInfoRequest {
return &mockContextInfoRequest{&request{s}}
}
func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte {
data, _ := json.Marshal(c)
return data
}
func (c *mockContextInfoRequest) GetContent() []byte {
data, _ := json.Marshal(c.Request)
return data
}
func Test_CallTarget(t *testing.T) {
type args struct {
ctx context.Context
info *middleware.ContextInfoRequest
server *callTestServer
target *mockTarget
sleep time.Duration
info ContextInfoRequest
method string
body []byte
respBody []byte
statusCode int
}
type res struct {
body []byte
@@ -234,16 +124,18 @@ func Test_CallTarget(t *testing.T) {
{
"unknown targettype, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
timeout: time.Second,
statusCode: http.StatusInternalServerError,
},
target: &mockTarget{
TargetType: 4,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
@@ -252,17 +144,19 @@ func Test_CallTarget(t *testing.T) {
{
"webhook, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
timeout: time.Second,
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
@@ -271,17 +165,19 @@ func Test_CallTarget(t *testing.T) {
{
"webhook, ok",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
timeout: time.Second,
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
res{
body: nil,
@@ -290,17 +186,19 @@ func Test_CallTarget(t *testing.T) {
{
"request response, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
timeout: time.Second,
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
target: &mockTarget{
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
@@ -309,17 +207,19 @@ func Test_CallTarget(t *testing.T) {
{
"request response, ok",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
timeout: time.Second,
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
target: &mockTarget{
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
res{
body: []byte("{\"request\":\"content2\"}"),
@@ -328,14 +228,7 @@ func Test_CallTarget(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
respBody, err := testServerCall(t,
tt.args.method,
tt.args.body,
tt.args.sleep,
tt.args.statusCode,
tt.args.respBody,
testCallTarget(tt.args.ctx, tt.args.target, tt.args.info),
)
respBody, err := testServer(t, tt.args.server, testCallTarget(tt.args.ctx, tt.args.info, tt.args.target))
if tt.res.wantErr {
assert.Error(t, err)
} else {
@@ -345,3 +238,278 @@ func Test_CallTarget(t *testing.T) {
})
}
}
func Test_CallTargets(t *testing.T) {
type args struct {
ctx context.Context
info *middleware.ContextInfoRequest
servers []*callTestServer
targets []*mockTarget
}
type res struct {
ret interface{}
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"interrupt on status",
args{
ctx: context.Background(),
info: requestContextInfo1,
servers: []*callTestServer{{
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: requestContextInfoBody2,
statusCode: http.StatusInternalServerError,
}, {
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: requestContextInfoBody2,
statusCode: http.StatusInternalServerError,
}},
targets: []*mockTarget{
{InterruptOnError: false},
{InterruptOnError: true},
},
},
res{
wantErr: true,
},
},
{
"continue on status",
args{
ctx: context.Background(),
info: requestContextInfo1,
servers: []*callTestServer{{
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: requestContextInfoBody2,
statusCode: http.StatusInternalServerError,
}, {
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: requestContextInfoBody2,
statusCode: http.StatusInternalServerError,
}},
targets: []*mockTarget{
{InterruptOnError: false},
{InterruptOnError: false},
},
},
res{
ret: requestContextInfo1.GetContent(),
},
},
{
"interrupt on json error",
args{
ctx: context.Background(),
info: requestContextInfo1,
servers: []*callTestServer{{
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: requestContextInfoBody2,
statusCode: http.StatusOK,
}, {
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: []byte("just a string, not json"),
statusCode: http.StatusOK,
}},
targets: []*mockTarget{
{InterruptOnError: false},
{InterruptOnError: true},
},
},
res{
wantErr: true,
},
},
{
"continue on json error",
args{
ctx: context.Background(),
info: requestContextInfo1,
servers: []*callTestServer{{
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: requestContextInfoBody2,
statusCode: http.StatusOK,
}, {
timeout: time.Second,
method: http.MethodPost,
expectBody: requestContextInfoBody1,
respondBody: []byte("just a string, not json"),
statusCode: http.StatusOK,
}},
targets: []*mockTarget{
{InterruptOnError: false},
{InterruptOnError: false},
}},
res{
ret: requestContextInfo1.GetContent(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
respBody, err := testServers(t,
tt.args.servers,
testCallTargets(tt.args.ctx, tt.args.info, tt.args.targets),
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
fmt.Println(respBody)
assert.Equal(t, tt.res.ret, respBody)
})
}
}
var _ execution.Target = &mockTarget{}
type mockTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *mockTarget) GetTargetID() string {
return e.TargetID
}
func (e *mockTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *mockTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *mockTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *mockTarget) GetTimeout() time.Duration {
return e.Timeout
}
type callTestServer struct {
method string
expectBody []byte
timeout time.Duration
statusCode int
respondBody []byte
}
func testServers(
t *testing.T,
c []*callTestServer,
call func([]string) (interface{}, error),
) (interface{}, error) {
urls := make([]string, len(c))
for i := range c {
url, close := listen(t, c[i])
defer close()
urls[i] = url
}
return call(urls)
}
func testServer(
t *testing.T,
c *callTestServer,
call func(string) ([]byte, error),
) ([]byte, error) {
url, close := listen(t, c)
defer close()
return call(url)
}
func listen(
t *testing.T,
c *callTestServer,
) (url string, close func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
checkRequest(t, r, c.method, c.expectBody)
if c.statusCode != http.StatusOK {
http.Error(w, "error", c.statusCode)
return
}
time.Sleep(c.timeout)
w.Header().Set("Content-Type", "application/json")
if _, err := io.WriteString(w, string(c.respondBody)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
return server.URL, server.Close
}
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
sentBody, err := io.ReadAll(sent.Body)
require.NoError(t, err)
require.Equal(t, expectedBody, sentBody)
require.Equal(t, method, sent.Method)
}
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
return func(url string) ([]byte, error) {
return execution.Call(ctx, url, timeout, body)
}
}
func testCallTarget(ctx context.Context,
info *middleware.ContextInfoRequest,
target *mockTarget,
) func(string) ([]byte, error) {
return func(url string) (r []byte, err error) {
target.Endpoint = url
return execution.CallTarget(ctx, target, info)
}
}
func testCallTargets(ctx context.Context,
info *middleware.ContextInfoRequest,
target []*mockTarget,
) func([]string) (interface{}, error) {
return func(urls []string) (interface{}, error) {
targets := make([]execution.Target, len(target))
for i, t := range target {
t.Endpoint = urls[i]
targets[i] = t
}
return execution.CallTargets(ctx, targets, info)
}
}
var requestContextInfo1 = &middleware.ContextInfoRequest{
Request: &request{
Request: "content1",
},
}
var requestContextInfoBody1 = []byte("{\"request\":{\"request\":\"content1\"}}")
var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}")
type request struct {
Request string `json:"request"`
}

View File

@@ -9,6 +9,9 @@ import (
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha"
)
// Details is the interface that covers both v1 and v2 proto generated object details.
@@ -63,6 +66,31 @@ func AssertDetails[D Details, M DetailsMsg[D]](t testing.TB, expected, actual M)
assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner())
}
func AssertResourceDetails(t testing.TB, expected *resources_object.Details, actual *resources_object.Details) {
assert.NotZero(t, actual.GetSequence())
if expected.GetChangeDate() != nil {
wantChangeDate := time.Now()
gotChangeDate := actual.GetChangeDate().AsTime()
assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute))
}
assert.Equal(t, expected.GetOwner(), actual.GetOwner())
assert.NotEmpty(t, actual.GetId())
}
func AssertSettingsDetails(t testing.TB, expected *settings_object.Details, actual *settings_object.Details) {
assert.NotZero(t, actual.GetSequence())
if expected.GetChangeDate() != nil {
wantChangeDate := time.Now()
gotChangeDate := actual.GetChangeDate().AsTime()
assert.WithinRange(t, gotChangeDate, wantChangeDate.Add(-time.Minute), wantChangeDate.Add(time.Minute))
}
assert.Equal(t, expected.GetOwner(), actual.GetOwner())
}
func AssertListDetails[L ListDetails, D ListDetailsMsg[L]](t testing.TB, expected, actual D) {
wantDetails, gotDetails := expected.GetDetails(), actual.GetDetails()
var nilDetails L

View File

@@ -25,21 +25,20 @@ import (
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/repository/idp"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
organisation "github.com/zitadel/zitadel/pkg/grpc/org/v2"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/settings/v2"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2"
settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
@@ -62,9 +61,9 @@ type Client struct {
OIDCv2beta oidc_pb_v2beta.OIDCServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2beta org_v2beta.OrganizationServiceClient
OrgV2 organisation.OrganizationServiceClient
OrgV2 org.OrganizationServiceClient
System system.SystemServiceClient
ActionV3 action.ActionServiceClient
ActionV3 action.ZITADELActionsClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 schema.UserSchemaServiceClient
@@ -85,9 +84,9 @@ func newClient(cc *grpc.ClientConn) Client {
OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
OrgV2: organisation.NewOrganizationServiceClient(cc),
OrgV2: org.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
ActionV3: action.NewActionServiceClient(cc),
ActionV3: action.NewZITADELActionsClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: schema.NewUserSchemaServiceClient(cc),
@@ -627,50 +626,52 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint
if name != "" {
nameSet = name
}
req := &action.CreateTargetRequest{
reqTarget := &action.Target{
Name: nameSet,
Endpoint: endpoint,
Timeout: durationpb.New(10 * time.Second),
}
switch ty {
case domain.TargetTypeWebhook:
req.TargetType = &action.CreateTargetRequest_RestWebhook{
reqTarget.TargetType = &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: interrupt,
},
}
case domain.TargetTypeCall:
req.TargetType = &action.CreateTargetRequest_RestCall{
reqTarget.TargetType = &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: interrupt,
},
}
case domain.TargetTypeAsync:
req.TargetType = &action.CreateTargetRequest_RestAsync{
reqTarget.TargetType = &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
}
}
target, err := s.Client.ActionV3.CreateTarget(ctx, req)
require.NoError(t, err)
return target
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Targets: targets,
})
target, err := s.Client.ActionV3.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
require.NoError(t, err)
return target
}
func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
_, err := s.Client.ActionV3.DeleteExecution(ctx, &action.DeleteExecutionRequest{
_, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
})
require.NoError(t, err)
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Execution: &action.Execution{
Targets: targets,
},
})
require.NoError(t, err)
return target
}
func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse {
return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1))
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
@@ -56,6 +57,13 @@ type Target struct {
Target string `json:"target"`
}
func (t *Target) Validate() error {
if t.Type == domain.ExecutionTargetTypeUnspecified || t.Target == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-hdm4zl1hmd", "Errors.Execution.Invalid")
}
return nil
}
func NewSetEventV2(
ctx context.Context,
aggregate *eventstore.Aggregate,

View File

@@ -580,6 +580,7 @@ Errors:
NotFound: Изпълнението не е намерено
IncludeNotFound: Включването не е намерено
NoTargets: Няма определени цели
ResponseIsNotValidJSON: Отговорът не е валиден JSON
UserSchema:
NotEnabled: Функцията „Потребителска схема“ не е активирана
Type:

View File

@@ -561,6 +561,7 @@ Errors:
NotFound: Provedení nenalezeno
IncludeNotFound: Zahrnout nenalezeno
NoTargets: Nejsou definovány žádné cíle
ResponseIsNotValidJSON: Odpověď není platný JSON
UserSchema:
NotEnabled: Funkce "Uživatelské schéma" není povolena
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Ausführung nicht gefunden
IncludeNotFound: Einschließen nicht gefunden
NoTargets: Keine Ziele definiert
ResponseIsNotValidJSON: Antwort ist kein gültiges JSON
UserSchema:
NotEnabled: Funktion Benutzerschema ist nicht aktiviert
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Execution not found
IncludeNotFound: Include not found
NoTargets: No targets defined
ResponseIsNotValidJSON: Response is not valid JSON
UserSchema:
NotEnabled: Feature "User Schema" is not enabled
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Ejecución no encontrada
IncludeNotFound: Incluir no encontrado
NoTargets: No hay objetivos definidos
ResponseIsNotValidJSON: La respuesta no es un JSON válido
UserSchema:
NotEnabled: La función "Esquema de usuario" no está habilitada
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Exécution introuvable
IncludeNotFound: Inclure introuvable
NoTargets: Aucune cible définie
ResponseIsNotValidJSON: La réponse n'est pas un JSON valide
UserSchema:
NotEnabled: La fonctionnalité "Schéma utilisateur" n'est pas activée
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Esecuzione non trovata
IncludeNotFound: Includi non trovato
NoTargets: Nessun obiettivo definito
ResponseIsNotValidJSON: La risposta non è un JSON valido
UserSchema:
NotEnabled: La funzionalità "Schema utente" non è abilitata
Type:

View File

@@ -552,6 +552,7 @@ Errors:
NotFound: 実行が見つかりませんでした
IncludeNotFound: 見つからないものを含める
NoTargets: ターゲットが定義されていません
ResponseIsNotValidJSON: 応答は有効な JSON ではありません
UserSchema:
NotEnabled: 機能「ユーザースキーマ」が有効になっていません
Type:

View File

@@ -562,6 +562,7 @@ Errors:
NotFound: Извршувањето не е пронајдено
IncludeNotFound: Вклучете не е пронајден
NoTargets: Не се дефинирани цели
ResponseIsNotValidJSON: Одговорот не е валиден JSON
UserSchema:
NotEnabled: Функцијата „Корисничка шема“ не е овозможена
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Uitvoering niet gevonden
IncludeNotFound: Inclusief niet gevonden
NoTargets: Geen doelstellingen gedefinieerd
ResponseIsNotValidJSON: Reactie is geen geldige JSON
UserSchema:
NotEnabled: Functie "Gebruikersschema" is niet ingeschakeld
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Nie znaleziono wykonania
IncludeNotFound: Nie znaleziono uwzględnienia
NoTargets: Nie zdefiniowano celów
ResponseIsNotValidJSON: Odpowiedź nie jest prawidłowym JSON-em
UserSchema:
NotEnabled: Funkcja „Schemat użytkownika” nie jest włączona
Type:

View File

@@ -558,6 +558,7 @@ Errors:
NotFound: Execução não encontrada
IncludeNotFound: Incluir não encontrado
NoTargets: Nenhuma meta definida
ResponseIsNotValidJSON: A resposta não é um JSON válido
UserSchema:
NotEnabled: O recurso "Esquema do usuário" não está habilitado
Type:

View File

@@ -552,6 +552,7 @@ Errors:
NotFound: Исполнение не найдено
IncludeNotFound: Включить не найдено
NoTargets: Цели не определены
ResponseIsNotValidJSON: Ответ не является допустимым JSON
UserSchema:
NotEnabled: Функция «Пользовательская схема» не включена
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: Exekveringen hittades inte
IncludeNotFound: Inkluderingen hittades inte
NoTargets: Inga mål definierade
ResponseIsNotValidJSON: Svaret är inte giltigt JSON
UserSchema:
NotEnabled: Funktionen "Användarschema" är inte aktiverad
Type:

View File

@@ -563,6 +563,7 @@ Errors:
NotFound: 未找到执行
IncludeNotFound: 包括未找到的内容
NoTargets: 没有定义目标
ResponseIsNotValidJSON: 响应不是有效的 JSON
UserSchema:
NotEnabled: 未启用“用户架构”功能
Type: