diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 786a829381..58847e2334 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1041,12 +1041,9 @@ InternalAuthZ: - "events.read" - "milestones.read" - "session.delete" - - "execution.target.read" - - "execution.target.write" - - "execution.target.delete" - - "execution.read" - - "execution.write" - - "execution.delete" + - "action.target.write" + - "action.target.delete" + - "action.execution.write" - "userschema.read" - "userschema.write" - "userschema.delete" diff --git a/cmd/start/start.go b/cmd/start/start.go index 0969c5388a..6bed1168dc 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -34,7 +34,6 @@ import ( "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" internal_authz "github.com/zitadel/zitadel/internal/api/authz" - action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/action/v3alpha" "github.com/zitadel/zitadel/internal/api/grpc/admin" "github.com/zitadel/zitadel/internal/api/grpc/auth" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" @@ -44,6 +43,7 @@ import ( oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" + action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha" session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2" session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta" settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2" diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c7378c63ec..c067ef25c4 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -333,7 +333,7 @@ module.exports = { }, }, action_v3: { - specPath: ".artifacts/openapi/zitadel/action/v3alpha/action_service.swagger.json", + specPath: ".artifacts/openapi/zitadel/resources/action/v3alpha/action_service.swagger.json", outputDir: "docs/apis/resources/action_service_v3", sidebarOptions: { groupPathsBy: "tag", diff --git a/internal/api/grpc/action/v3alpha/execution_integration_test.go b/internal/api/grpc/action/v3alpha/execution_integration_test.go deleted file mode 100644 index 2a01e40383..0000000000 --- a/internal/api/grpc/action/v3alpha/execution_integration_test.go +++ /dev/null @@ -1,1322 +0,0 @@ -//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" - action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" - "github.com/zitadel/zitadel/pkg/grpc/object/v2" -) - -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{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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.v2.NotExistingService/List", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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.v2.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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.v2.SessionService", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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, - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - 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.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // 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.GetId()), - ) - - circularExecutionService := &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.session.v2.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.v2.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, - 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.v2.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleInclude(executionCond), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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.v2.SessionService", - }, - }, - }, - }, - Targets: executionTargetsSingleInclude(executionCond), - }, - want: &action.SetExecutionResponse{ - 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.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Request(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - 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.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{}, - }, - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/NotExisting", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_Service{ - Service: "zitadel.user.v2.UserService", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} - -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{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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.v2.NotExistingService/List", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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.v2.SessionService/ListSessions", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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.v2.SessionService", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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, - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - 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.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Response(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - 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.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - }, - wantErr: true, - }, - { - name: "method, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2.SessionService/NotExisting", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "method, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Method{ - Method: "/zitadel.session.v2.SessionService/GetSession", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "service, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "NotExistingService", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "service, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_Service{ - Service: "zitadel.user.v2.UserService", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{ - Condition: &action.ResponseExecution_All{ - All: true, - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} - -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{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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", - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: 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, - }, - }, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - 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.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Event(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - 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.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{}, - }, - }, - }, - wantErr: true, - }, - /* - //TODO: add when check is implemented - { - name: "event, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - }, - wantErr: true, - }, - */ - { - name: "event, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Event{ - Event: "xxx", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "group, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "group, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_Group{ - Group: "xxx", - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - { - name: "all, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "all, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Event{ - Event: &action.EventExecution{ - Condition: &action.EventExecution_All{ - All: true, - }, - }, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} - -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{}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - wantErr: true, - }, - { - name: "function, not existing", - ctx: CTX, - req: &action.SetExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "xxx"}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.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"}, - }, - }, - Targets: executionTargetsSingleTarget(targetResp.GetId()), - }, - want: &action.SetExecutionResponse{ - 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.SetExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - - // cleanup to not impact other requests - Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) - }) - } -} - -func TestServer_DeleteExecution_Function(t *testing.T) { - ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) - - tests := []struct { - name string - ctx context.Context - dep func(ctx context.Context, request *action.DeleteExecutionRequest) error - req *action.DeleteExecutionRequest - want *action.DeleteExecutionResponse - wantErr bool - }{ - { - name: "missing permission", - ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), - req: &action.DeleteExecutionRequest{ - 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.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Response{ - Response: &action.ResponseExecution{}, - }, - }, - }, - wantErr: true, - }, - { - name: "function, not existing", - ctx: CTX, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "xxx"}, - }, - }, - }, - wantErr: true, - }, - { - name: "function, ok", - ctx: CTX, - dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) - return nil - }, - req: &action.DeleteExecutionRequest{ - Condition: &action.Condition{ - ConditionType: &action.Condition_Function{ - Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, - }, - }, - }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.dep != nil { - err := tt.dep(tt.ctx, tt.req) - require.NoError(t, err) - } - - got, err := Client.DeleteExecution(tt.ctx, tt.req) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - integration.AssertDetails(t, tt.want, got) - }) - } -} diff --git a/internal/api/grpc/action/v3alpha/execution_target_integration_test.go b/internal/api/grpc/action/v3alpha/execution_target_integration_test.go deleted file mode 100644 index 30afb1af6f..0000000000 --- a/internal/api/grpc/action/v3alpha/execution_target_integration_test.go +++ /dev/null @@ -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 -} diff --git a/internal/api/grpc/action/v3alpha/query.go b/internal/api/grpc/action/v3alpha/query.go deleted file mode 100644 index 095eaa7973..0000000000 --- a/internal/api/grpc/action/v3alpha/query.go +++ /dev/null @@ -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, "/")}}} -} diff --git a/internal/api/grpc/action/v3alpha/query_integration_test.go b/internal/api/grpc/action/v3alpha/query_integration_test.go deleted file mode 100644 index 279109ef78..0000000000 --- a/internal/api/grpc/action/v3alpha/query_integration_test.go +++ /dev/null @@ -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 -} diff --git a/internal/api/grpc/action/v3alpha/target.go b/internal/api/grpc/action/v3alpha/target.go deleted file mode 100644 index c57d5b607f..0000000000 --- a/internal/api/grpc/action/v3alpha/target.go +++ /dev/null @@ -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 -} diff --git a/internal/api/grpc/action/v3alpha/target_integration_test.go b/internal/api/grpc/action/v3alpha/target_integration_test.go deleted file mode 100644 index 539f3c6d35..0000000000 --- a/internal/api/grpc/action/v3alpha/target_integration_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/internal/api/grpc/action/v3alpha/execution.go b/internal/api/grpc/resources/action/v3alpha/execution.go similarity index 64% rename from internal/api/grpc/action/v3alpha/execution.go rename to internal/api/grpc/resources/action/v3alpha/execution.go index 58a36cff22..668b6b5261 100644 --- a/internal/api/grpc/action/v3alpha/execution.go +++ b/internal/api/grpc/resources/action/v3alpha/execution.go @@ -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 } diff --git a/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go new file mode 100644 index 0000000000..9713a3c578 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/execution_integration_test.go @@ -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()) + }) + } +} diff --git a/internal/api/grpc/action/v3alpha/server.go b/internal/api/grpc/resources/action/v3alpha/server.go similarity index 77% rename from internal/api/grpc/action/v3alpha/server.go rename to internal/api/grpc/resources/action/v3alpha/server.go index 952a555d24..57d0761fd2 100644 --- a/internal/api/grpc/action/v3alpha/server.go +++ b/internal/api/grpc/resources/action/v3alpha/server.go @@ -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 { diff --git a/internal/api/grpc/action/v3alpha/server_integration_test.go b/internal/api/grpc/resources/action/v3alpha/server_integration_test.go similarity index 90% rename from internal/api/grpc/action/v3alpha/server_integration_test.go rename to internal/api/grpc/resources/action/v3alpha/server_integration_test.go index e97605e1f0..483ed2bd3f 100644 --- a/internal/api/grpc/action/v3alpha/server_integration_test.go +++ b/internal/api/grpc/resources/action/v3alpha/server_integration_test.go @@ -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) { diff --git a/internal/api/grpc/resources/action/v3alpha/target.go b/internal/api/grpc/resources/action/v3alpha/target.go new file mode 100644 index 0000000000..5d33dac911 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/target.go @@ -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(), + } +} diff --git a/internal/api/grpc/resources/action/v3alpha/target_integration_test.go b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go new file mode 100644 index 0000000000..bda54bf862 --- /dev/null +++ b/internal/api/grpc/resources/action/v3alpha/target_integration_test.go @@ -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) + }) + } +} diff --git a/internal/api/grpc/action/v3alpha/target_test.go b/internal/api/grpc/resources/action/v3alpha/target_test.go similarity index 83% rename from internal/api/grpc/action/v3alpha/target_test.go rename to internal/api/grpc/resources/action/v3alpha/target_test.go index 23e33ad9be..f4e0d02e3b 100644 --- a/internal/api/grpc/action/v3alpha/target_test.go +++ b/internal/api/grpc/resources/action/v3alpha/target_test.go @@ -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) }) } diff --git a/internal/api/grpc/resources/object/v3alpha/converter.go b/internal/api/grpc/resources/object/v3alpha/converter.go new file mode 100644 index 0000000000..41f81b595f --- /dev/null +++ b/internal/api/grpc/resources/object/v3alpha/converter.go @@ -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 +} diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go index ec4eee17d2..c309827d94 100644 --- a/internal/api/grpc/server/middleware/execution_interceptor.go +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -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) } diff --git a/internal/api/grpc/settings/object/v3alpha/converter.go b/internal/api/grpc/settings/object/v3alpha/converter.go new file mode 100644 index 0000000000..c11c14ea63 --- /dev/null +++ b/internal/api/grpc/settings/object/v3alpha/converter.go @@ -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 +} diff --git a/internal/command/action_v2_execution.go b/internal/command/action_v2_execution.go index 7fb08a4a32..6e0dda4ef2 100644 --- a/internal/command/action_v2_execution.go +++ b/internal/command/action_v2_execution.go @@ -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) diff --git a/internal/command/action_v2_execution_model.go b/internal/command/action_v2_execution_model.go index 30cab0f56e..5e678ed4d7 100644 --- a/internal/command/action_v2_execution_model.go +++ b/internal/command/action_v2_execution_model.go @@ -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 { diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index 5a9c0ecb1d..eb6cd21c31 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -90,26 +90,6 @@ func TestCommands_SetExecutionRequest(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - "empty executionType, error", - fields{ - eventstore: expectEventstore(), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { "empty target, error", fields{ @@ -123,7 +103,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -182,6 +162,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, method target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -229,6 +210,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, service target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -276,6 +258,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, all target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -322,7 +305,8 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push not found, method include", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), grpcMethodExists: existsMock(true), }, @@ -348,6 +332,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, method include", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( execution.NewSetEventV2(context.Background(), @@ -403,7 +388,8 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push not found, service include", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), grpcServiceExists: existsMock(true), }, @@ -429,6 +415,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, service include", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( execution.NewSetEventV2(context.Background(), @@ -484,7 +471,8 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push not found, all include", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), }, args{ @@ -509,6 +497,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { "push ok, all include", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( execution.NewSetEventV2(context.Background(), @@ -559,6 +548,83 @@ func TestCommands_SetExecutionRequest(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -652,26 +718,6 @@ func TestCommands_SetExecutionResponse(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - "empty executionType, error", - fields{ - eventstore: expectEventstore(), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { "empty target, error", fields{ @@ -685,7 +731,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -696,6 +742,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push failed, error", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( target.NewAddedEvent(context.Background(), target.NewAggregate("target", "instance"), @@ -788,6 +835,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push ok, method target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), @@ -835,6 +883,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push ok, service target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -873,6 +922,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { "push ok, all target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -906,6 +956,83 @@ func TestCommands_SetExecutionResponse(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1012,7 +1139,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{Target: "target"}}}, resourceOwner: "instance", }, res{ @@ -1032,7 +1159,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "", false, }, - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -1043,6 +1170,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push failed, error", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1128,6 +1256,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push ok, event target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1166,6 +1295,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push ok, group target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1204,6 +1334,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { "push ok, all target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1237,6 +1368,83 @@ func TestCommands_SetExecutionEvent(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1311,22 +1519,6 @@ func TestCommands_SetExecutionFunction(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - "empty executionType, error", - fields{ - eventstore: expectEventstore(), - actionFunctionExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: "function", - set: &SetExecution{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { "empty target, error", fields{ @@ -1336,7 +1528,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { args{ ctx: context.Background(), cond: "function", - set: &SetExecution{}, + set: &SetExecution{Targets: []*execution.Target{{}}}, resourceOwner: "instance", }, res{ @@ -1347,6 +1539,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { "push failed, error", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1379,7 +1572,8 @@ func TestCommands_SetExecutionFunction(t *testing.T) { "push error, function target", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter(), // execution doesn't exist yet + expectFilter(), // target doesn't exist ), actionFunctionExists: existsMock(true), }, @@ -1421,6 +1615,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { "push ok, function target", fields{ eventstore: expectEventstore( + expectFilter(), // execution doesn't exist yet expectFilter( targetAddEvent("target", "instance"), ), @@ -1451,6 +1646,77 @@ func TestCommands_SetExecutionFunction(t *testing.T) { }, }, }, + { + "push ok, remove all targets", + fields{ + actionFunctionExists: existsMock(true), + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + expectPush( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []*execution.Target{}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, + { + "push ok, unchanged execution", + fields{ + actionFunctionExists: existsMock(true), + eventstore: expectEventstore( + expectFilter( // execution has targets + eventFromEventPusher( + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []*execution.Target{{ + Type: domain.ExecutionTargetTypeTarget, + Target: "target", + }}, + }, + resourceOwner: "instance", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "instance", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1472,938 +1738,6 @@ func TestCommands_SetExecutionFunction(t *testing.T) { } } -func TestCommands_DeleteExecutionRequest(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond *ExecutionAPICondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no valid cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "notvalid", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "not found, error", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, method target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request/method", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request/method", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, service target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request/service", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request/service", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "service", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, all target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("request", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionRequest(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - -func TestCommands_DeleteExecutionResponse(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond *ExecutionAPICondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no valid cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "notvalid", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "not found, error", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, method target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response/method", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response/method", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "method", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, service target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response/service", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response/service", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "service", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push ok, all target", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("response", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionResponse(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - -func TestCommands_DeleteExecutionEvent(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond *ExecutionEventCondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{}, - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{}, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "push error, not existing", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push error, event", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, event", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event/valid", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event/valid", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "valid", - "", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push error, group", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "valid", - false, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, group", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event/group", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event/group.*", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "group", - false, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - { - "push error, all", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, all", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("event", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "", - "", - true, - }, - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionEvent(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - -func TestCommands_DeleteExecutionFunction(t *testing.T) { - type fields struct { - eventstore func(t *testing.T) *eventstore.Eventstore - } - type args struct { - ctx context.Context - cond ExecutionFunctionCondition - resourceOwner string - } - type res struct { - details *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - "no resourceowner, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: "", - resourceOwner: "", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "no cond, error", - fields{ - eventstore: expectEventstore(), - }, - args{ - ctx: context.Background(), - cond: "", - resourceOwner: "instance", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("function/function", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function/function", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: "function", - resourceOwner: "instance", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, - { - "push error, not existing", - fields{ - eventstore: expectEventstore( - expectFilter(), - ), - }, - args{ - ctx: context.Background(), - cond: "function", - resourceOwner: "instance", - }, - res{ - err: zerrors.IsNotFound, - }, - }, - { - "push ok, function", - fields{ - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - execution.NewSetEventV2(context.Background(), - execution.NewAggregate("function/function", "instance"), - []*execution.Target{ - {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, - }, - ), - ), - ), - expectPush( - execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function/function", "instance"), - ), - ), - ), - }, - args{ - ctx: context.Background(), - cond: "function", - resourceOwner: "instance", - }, - res{ - details: &domain.ObjectDetails{ - ResourceOwner: "instance", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.fields.eventstore(t), - } - details, err := c.DeleteExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.details, details) - } - }) - } -} - func mockExecutionIncludesCache(cache map[string][]string) includeCacheFunc { return func(ctx context.Context, id string, resourceOwner string) ([]string, error) { included, ok := cache[id] diff --git a/internal/execution/execution.go b/internal/execution/execution.go index abb2153fc2..c493673e90 100644 --- a/internal/execution/execution.go +++ b/internal/execution/execution.go @@ -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() { diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go index 2d891148df..4a68ff5ac8 100644 --- a/internal/execution/execution_test.go +++ b/internal/execution/execution_test.go @@ -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"` +} diff --git a/internal/integration/assert.go b/internal/integration/assert.go index 6928054e8e..682a82ff7f 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -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 diff --git a/internal/integration/client.go b/internal/integration/client.go index 7cb3af4c7b..3819682618 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -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)) } diff --git a/internal/repository/execution/execution.go b/internal/repository/execution/execution.go index a6851b4495..855d646e08 100644 --- a/internal/repository/execution/execution.go +++ b/internal/repository/execution/execution.go @@ -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, diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 007a20bf53..1161a4928d 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -580,6 +580,7 @@ Errors: NotFound: Изпълнението не е намерено IncludeNotFound: Включването не е намерено NoTargets: Няма определени цели + ResponseIsNotValidJSON: Отговорът не е валиден JSON UserSchema: NotEnabled: Функцията „Потребителска схема“ не е активирана Type: diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index b119e1bba7..3383021b48 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -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: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 518e1ec501..e9ebccf3bf 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -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: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 7cedac9fd6..a35cfc043d 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -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: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index bae735134e..b4ba11cfaa 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -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: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 9c8e215e25..e10df340da 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -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: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index dcc0fab2f3..a853e28748 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -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: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index a4224571ec..725cdcc7ab 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -552,6 +552,7 @@ Errors: NotFound: 実行が見つかりませんでした IncludeNotFound: 見つからないものを含める NoTargets: ターゲットが定義されていません + ResponseIsNotValidJSON: 応答は有効な JSON ではありません UserSchema: NotEnabled: 機能「ユーザースキーマ」が有効になっていません Type: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 8102a4d557..d7aabafe3d 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -562,6 +562,7 @@ Errors: NotFound: Извршувањето не е пронајдено IncludeNotFound: Вклучете не е пронајден NoTargets: Не се дефинирани цели + ResponseIsNotValidJSON: Одговорот не е валиден JSON UserSchema: NotEnabled: Функцијата „Корисничка шема“ не е овозможена Type: diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 11a9510d0d..fa5e3b6ce5 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -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: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index d5d688e021..c6081de5a7 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -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: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 59506fc1d0..e980b4ea21 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -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: diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 7bde4b3f2b..b2aa62d28f 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -552,6 +552,7 @@ Errors: NotFound: Исполнение не найдено IncludeNotFound: Включить не найдено NoTargets: Цели не определены + ResponseIsNotValidJSON: Ответ не является допустимым JSON UserSchema: NotEnabled: Функция «Пользовательская схема» не включена Type: diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index 1540c63a1d..d995df06f5 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -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: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 4c78a33cb7..b4fa6e90be 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -563,6 +563,7 @@ Errors: NotFound: 未找到执行 IncludeNotFound: 包括未找到的内容 NoTargets: 没有定义目标 + ResponseIsNotValidJSON: 响应不是有效的 JSON UserSchema: NotEnabled: 未启用“用户架构”功能 Type: diff --git a/proto/zitadel/action/v3alpha/action_service.proto b/proto/zitadel/action/v3alpha/action_service.proto deleted file mode 100644 index 938c9e88fc..0000000000 --- a/proto/zitadel/action/v3alpha/action_service.proto +++ /dev/null @@ -1,612 +0,0 @@ -syntax = "proto3"; - -package zitadel.action.v3alpha; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/action/v3alpha/target.proto"; -import "zitadel/action/v3alpha/execution.proto"; -import "zitadel/action/v3alpha/query.proto"; -import "zitadel/object/v2/object.proto"; -import "zitadel/protoc_gen_zitadel/v2/options.proto"; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; - -option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "Action Service"; - version: "3.0-preview"; - description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. This project is in preview state. It can AND will continue breaking until the services provide the same functionality as the current actions."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; - }; - schemes: HTTPS; - schemes: HTTP; - - consumes: "application/json"; - consumes: "application/grpc"; - - produces: "application/json"; - produces: "application/grpc"; - - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; - - host: "$CUSTOM-DOMAIN"; - base_path: "/"; - - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://zitadel.com/docs" - } - security_definitions: { - security: { - key: "OAuth2"; - value: { - type: TYPE_OAUTH2; - flow: FLOW_ACCESS_CODE; - authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; - token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; - scopes: { - scope: { - key: "openid"; - value: "openid"; - } - scope: { - key: "urn:zitadel:iam:org:project:id:zitadel:aud"; - value: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - } - } - security: { - security_requirement: { - key: "OAuth2"; - value: { - scope: "openid"; - scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; - } - } - } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } -}; - -service ActionService { - - // Create a target - // - // Create a new target, which can be used in executions. - rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { - option (google.api.http) = { - post: "/v3alpha/targets" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.write" - } - http_response: { - success_code: 201 - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "201"; - value: { - description: "Target successfully created"; - schema: { - json_schema: { - ref: "#/definitions/v3alphaCreateTargetResponse"; - } - } - }; - }; - }; - } - - // Update a target - // - // Update an existing target. - rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) { - option (google.api.http) = { - put: "/v3alpha/targets/{target_id}" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully updated"; - }; - }; - }; - } - - // Delete a target - // - // Delete an existing target. This will remove it from any configured execution as well. - rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { - option (google.api.http) = { - delete: "/v3alpha/targets/{target_id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Target successfully deleted"; - }; - }; - }; - } - - // List targets - // - // List all matching targets. By default, we will return all targets of your instance. - // Make sure to include a limit and sorting for pagination. - rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { - option (google.api.http) = { - post: "/v3alpha/targets/search" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all targets matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // Target by ID - // - // Returns the target identified by the requested ID. - rpc GetTargetByID (GetTargetByIDRequest) returns (GetTargetByIDResponse) { - option (google.api.http) = { - get: "/v3alpha/targets/{target_id}" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.target.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200" - value: { - description: "Target successfully retrieved"; - } - }; - }; - } - - // Set an execution - // - // Set an execution to call a previously defined target or include the targets of a previously defined execution. - rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { - option (google.api.http) = { - put: "/v3alpha/executions" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.write" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully set"; - }; - }; - }; - } - - // Delete an execution - // - // Delete an existing execution. - rpc DeleteExecution (DeleteExecutionRequest) returns (DeleteExecutionResponse) { - option (google.api.http) = { - delete: "/v3alpha/executions" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.delete" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "Execution successfully deleted"; - }; - }; - }; - } - - // List executions - // - // List all matching executions. By default, we will return all executions of your instance. - // Make sure to include a limit and sorting for pagination. - rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { - option (google.api.http) = { - post: "/v3alpha/executions/search" - body: "*" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "A list of all executions matching the query"; - }; - }; - responses: { - key: "400"; - value: { - description: "invalid list query"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } - - // List all available functions - // - // List all available functions which can be used as condition for executions. - rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { - option (google.api.http) = { - get: "/v3alpha/executions/functions" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all functions successfully"; - }; - }; - }; - } - // List all available methods - // - // List all available methods which can be used as condition for executions. - rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { - option (google.api.http) = { - get: "/v3alpha/executions/methods" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all methods successfully"; - }; - }; - }; - } - // List all available service - // - // List all available services which can be used as condition for executions. - rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { - option (google.api.http) = { - get: "/v3alpha/executions/services" - }; - - option (zitadel.protoc_gen_zitadel.v2.options) = { - auth_option: { - permission: "execution.read" - } - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - responses: { - key: "200"; - value: { - description: "List all services successfully"; - }; - }; - }; - } -} - -message CreateTargetRequest { - // Unique name of the target. - string name = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"ip_allow_list\""; - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - option (validate.required) = true; - - SetRESTWebhook rest_webhook = 2; - SetRESTCall rest_call = 3; - SetRESTAsync rest_async = 4; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 5 [ - (validate.rules).duration = {gt: {seconds: 0}, required: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - string endpoint = 6 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} - -message CreateTargetResponse { - // ID is the read-only unique identifier of the target. - string id = 1; - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 2; -} - -message UpdateTargetRequest { - // unique identifier of the target. - string target_id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; - // Optionally change the unique name of the target. - optional string name = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"ip_allow_list\""; - } - ]; - // Optionally change the target type and how the response of the target is treated, - // or its target URL. - oneof target_type { - SetRESTWebhook rest_webhook = 3; - SetRESTCall rest_call = 4; - SetRESTAsync rest_async = 5; - } - // Optionally change the timeout, which defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 6 [ - (validate.rules).duration = {gt: {seconds: 0}}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - - optional string endpoint = 7 [ - (validate.rules).string = {max_len: 1000, uri: true}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} - -message UpdateTargetResponse { - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 1; -} - -message DeleteTargetRequest { - // unique identifier of the target. - string target_id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message DeleteTargetResponse { - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 1; -} - -message ListTargetsRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; - // the field the result is sorted. - zitadel.action.v3alpha.TargetFieldName sorting_column = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"FIELD_NAME_SCHEMA_TYPE\"" - } - ]; - // Define the criteria to query for. - repeated zitadel.action.v3alpha.TargetSearchQuery queries = 3; -} - -message ListTargetsResponse { - // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; - // States by which field the results are sorted. - zitadel.action.v3alpha.TargetFieldName sorting_column = 2; - // The result contains the user schemas, which matched the queries. - repeated zitadel.action.v3alpha.Target result = 3; -} - -message GetTargetByIDRequest { - // unique identifier of the target. - string target_id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 200, - example: "\"69629026806489455\""; - } - ]; -} - -message GetTargetByIDResponse { - zitadel.action.v3alpha.Target target = 1; -} - -message SetExecutionRequest { - // Defines the condition type and content of the condition for execution. - Condition condition = 1; - // Ordered list of targets/includes called during the execution. - repeated zitadel.action.v3alpha.ExecutionTargetType targets = 2; -} - -message SetExecutionResponse { - // Details provide some base information (such as the last change date) of the execution. - zitadel.object.v2.Details details = 2; -} - -message DeleteExecutionRequest { - // Unique identifier of the execution. - Condition condition = 1; -} - -message DeleteExecutionResponse { - // Details provide some base information (such as the last change date) of the execution. - zitadel.object.v2.Details details = 1; -} - -message ListExecutionsRequest { - // list limitations and ordering. - zitadel.object.v2.ListQuery query = 1; - // Define the criteria to query for. - repeated zitadel.action.v3alpha.SearchQuery queries = 2; -} - -message ListExecutionsResponse { - // Details provides information about the returned result including total amount found. - zitadel.object.v2.ListDetails details = 1; - // The result contains the executions, which matched the queries. - repeated zitadel.action.v3alpha.Execution result = 2; -} - -message ListExecutionFunctionsRequest{} -message ListExecutionFunctionsResponse{ - // All available methods - repeated string functions = 1; -} -message ListExecutionMethodsRequest{} -message ListExecutionMethodsResponse{ - // All available methods - repeated string methods = 1; -} - -message ListExecutionServicesRequest{} -message ListExecutionServicesResponse{ - // All available methods - repeated string services = 1; -} \ No newline at end of file diff --git a/proto/zitadel/action/v3alpha/query.proto b/proto/zitadel/action/v3alpha/query.proto deleted file mode 100644 index e32bda1d84..0000000000 --- a/proto/zitadel/action/v3alpha/query.proto +++ /dev/null @@ -1,110 +0,0 @@ -syntax = "proto3"; - -package zitadel.action.v3alpha; - -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; - -import "google/api/field_behavior.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; -import "zitadel/action/v3alpha/execution.proto"; - -message SearchQuery { - oneof query { - option (validate.required) = true; - - InConditionsQuery in_conditions_query = 1; - ExecutionTypeQuery execution_type_query = 2; - TargetQuery target_query = 3; - IncludeQuery include_query = 4; - } -} - -message InConditionsQuery { - // Defines the conditions to query for. - repeated Condition conditions = 1; -} - -message ExecutionTypeQuery { - // Defines the type to query for. - ExecutionType execution_type = 1; -} - -message TargetQuery { - // Defines the id to query for. - string target_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the targets to include" - example: "\"69629023906488334\""; - } - ]; -} - -message IncludeQuery { - // Defines the include to query for. - Condition include = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the id of the include" - example: "\"request.zitadel.session.v2.SessionService\""; - } - ]; -} - -message TargetSearchQuery { - oneof query { - option (validate.required) = true; - - TargetNameQuery target_name_query = 1; - InTargetIDsQuery in_target_ids_query = 2; - } -} - -message TargetNameQuery { - // Defines the name of the target to query for. - string target_name = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - max_length: 200; - example: "\"ip_allow_list\""; - } - ]; - // Defines which text comparison method used for the name query. - zitadel.object.v2.TextQueryMethod method = 2 [ - (validate.rules).enum.defined_only = true, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "defines which text equality method is used"; - } - ]; -} - -message InTargetIDsQuery { - // Defines the ids to query for. - repeated string target_ids = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "the ids of the targets to include" - example: "[\"69629023906488334\",\"69622366012355662\"]"; - } - ]; -} - -enum ExecutionType { - EXECUTION_TYPE_UNSPECIFIED = 0; - EXECUTION_TYPE_REQUEST = 1; - EXECUTION_TYPE_RESPONSE = 2; - EXECUTION_TYPE_EVENT = 3; - EXECUTION_TYPE_FUNCTION = 4; -} - -enum TargetFieldName { - FIELD_NAME_UNSPECIFIED = 0; - FIELD_NAME_ID = 1; - FIELD_NAME_CREATION_DATE = 2; - FIELD_NAME_CHANGE_DATE = 3; - FIELD_NAME_NAME = 4; - FIELD_NAME_TARGET_TYPE = 5; - FIELD_NAME_URL = 6; - FIELD_NAME_TIMEOUT = 7; - FIELD_NAME_ASYNC = 8; - FIELD_NAME_INTERRUPT_ON_ERROR = 9; -} diff --git a/proto/zitadel/object/v3alpha/object.proto b/proto/zitadel/object/v3alpha/object.proto new file mode 100644 index 0000000000..fba08fa5b4 --- /dev/null +++ b/proto/zitadel/object/v3alpha/object.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package zitadel.object.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha;object"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum OwnerType { + OWNER_TYPE_UNSPECIFIED = 0; + OWNER_TYPE_SYSTEM = 1; + OWNER_TYPE_INSTANCE = 2; + OWNER_TYPE_ORG = 3; +} + +message Owner { + OwnerType type = 1; + string id = 2; +} + diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto new file mode 100644 index 0000000000..a8ae67b95d --- /dev/null +++ b/proto/zitadel/resources/action/v3alpha/action_service.proto @@ -0,0 +1,361 @@ +syntax = "proto3"; + +package zitadel.resources.action.v3alpha; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/resources/action/v3alpha/target.proto"; +import "zitadel/resources/action/v3alpha/execution.proto"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "zitadel/settings/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Action Service"; + version: "3.0-alpha"; + description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. It will continue breaking as long as it is in alpha state."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$ZITADEL_DOMAIN"; + base_path: "/resources/v3alpha/actions"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +service ZITADELActions { + + // Create a target + // + // Create a new target, which can be used in executions. + rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { + option (google.api.http) = { + post: "/targets" + body: "target" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "201"; + value: { + description: "Target successfully created"; + schema: { + json_schema: { + ref: "#/definitions/CreateTargetResponse"; + } + } + }; + }; + }; + } + + // Patch a target + // + // Patch an existing target. + rpc PatchTarget (PatchTargetRequest) returns (PatchTargetResponse) { + option (google.api.http) = { + patch: "/targets/{id}" + body: "target" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully updated or left unchanged"; + }; + }; + }; + } + + // Delete a target + // + // Delete an existing target. This will remove it from any configured execution as well. + rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { + option (google.api.http) = { + delete: "/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully deleted"; + }; + }; + }; + } + + // Sets an execution to call a target or include the targets of another execution. + // + // Setting an empty list of targets will remove all targets from the execution, making it a noop. + rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { + option (google.api.http) = { + put: "/executions" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.write" + } + http_response: { + success_code: 201 + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Execution successfully updated or left unchanged"; + schema: { + json_schema: { + ref: "#/definitions/SetExecutionResponse"; + } + } + }; + }; + }; + } + + // List all available functions + // + // List all available functions which can be used as condition for executions. + rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { + option (google.api.http) = { + get: "/executions/functions" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all functions successfully"; + }; + }; + }; + } + // List all available methods + // + // List all available methods which can be used as condition for executions. + rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { + option (google.api.http) = { + get: "/executions/methods" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all methods successfully"; + }; + }; + }; + } + // List all available service + // + // List all available services which can be used as condition for executions. + rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { + option (google.api.http) = { + get: "/executions/services" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all services successfully"; + }; + }; + }; + } +} + +message CreateTargetRequest { + Target target = 1; +} + +message CreateTargetResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message PatchTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + PatchTarget target = 2; +} + +message PatchTargetResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message DeleteTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteTargetResponse { + zitadel.resources.object.v3alpha.Details details = 1; +} + +message SetExecutionRequest { + Condition condition = 1; + Execution execution = 2; +} + +message SetExecutionResponse { + zitadel.settings.object.v3alpha.Details details = 1; +} + +message ListExecutionFunctionsRequest{} +message ListExecutionFunctionsResponse{ + // All available methods + repeated string functions = 1; +} +message ListExecutionMethodsRequest{} +message ListExecutionMethodsResponse{ + // All available methods + repeated string methods = 1; +} + +message ListExecutionServicesRequest{} +message ListExecutionServicesResponse{ + // All available methods + repeated string services = 1; +} diff --git a/proto/zitadel/action/v3alpha/execution.proto b/proto/zitadel/resources/action/v3alpha/execution.proto similarity index 76% rename from proto/zitadel/action/v3alpha/execution.proto rename to proto/zitadel/resources/action/v3alpha/execution.proto index 797f997cd8..f666b4c497 100644 --- a/proto/zitadel/action/v3alpha/execution.proto +++ b/proto/zitadel/resources/action/v3alpha/execution.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.action.v3alpha; +package zitadel.resources.action.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,21 +8,22 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; +import "zitadel/resources/object/v3alpha/object.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/object/v3alpha/object.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; message Execution { - Condition Condition = 1; - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 2; - // List of ordered list of targets/includes called during the execution. - repeated ExecutionTargetType targets = 3; + // Ordered list of targets/includes called during the execution. + repeated ExecutionTargetType targets = 1; } message ExecutionTargetType { oneof type { + option (validate.required) = true; // Unique identifier of existing target to call. string target = 1; // Unique identifier of existing execution to include targets of. @@ -47,8 +48,9 @@ message Condition { } message RequestExecution { - // Condition for the request execution, only one possible. + // Condition for the request execution. Only one is possible. oneof condition{ + option (validate.required) = true; // GRPC-method as condition. string method = 1 [ (validate.rules).string = {min_len: 1, max_len: 1000}, @@ -67,14 +69,15 @@ message RequestExecution { example: "\"zitadel.session.v2.SessionService\""; } ]; - // All calls to any available service and endpoint as condition. - bool all = 3; + // All calls to any available services and methods as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; } } message ResponseExecution { - // Condition for the response execution, only one possible. + // Condition for the response execution. Only one is possible. oneof condition{ + option (validate.required) = true; // GRPC-method as condition. string method = 1 [ (validate.rules).string = {min_len: 1, max_len: 1000}, @@ -93,8 +96,8 @@ message ResponseExecution { example: "\"zitadel.session.v2.SessionService\""; } ]; - // All calls to any available service and endpoint as condition. - bool all = 3; + // All calls to any available services and methods as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; } } @@ -103,9 +106,10 @@ message FunctionExecution { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1000}]; } -message EventExecution{ - // Condition for the event execution, only one possible. +message EventExecution { + // Condition for the event execution. Only one is possible. oneof condition{ + option (validate.required) = true; // Event name as condition. string event = 1 [ (validate.rules).string = {min_len: 1, max_len: 1000}, @@ -125,7 +129,6 @@ message EventExecution{ } ]; // all events as condition. - bool all = 3; + bool all = 3 [(validate.rules).bool = {const: true}]; } } - diff --git a/proto/zitadel/action/v3alpha/target.proto b/proto/zitadel/resources/action/v3alpha/target.proto similarity index 60% rename from proto/zitadel/action/v3alpha/target.proto rename to proto/zitadel/resources/action/v3alpha/target.proto index bea5a4b756..20843a530b 100644 --- a/proto/zitadel/action/v3alpha/target.proto +++ b/proto/zitadel/resources/action/v3alpha/target.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package zitadel.action.v3alpha; +package zitadel.resources.action.v3alpha; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; @@ -8,10 +8,61 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; -import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; -option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha;action"; + +message Target { + string name = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + option (validate.required) = true; + SetRESTWebhook rest_webhook = 2; + SetRESTCall rest_call = 3; + SetRESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + google.protobuf.Duration timeout = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\""; + } + ]; +} + +message PatchTarget { + optional string name = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + SetRESTWebhook rest_webhook = 2; + SetRESTCall rest_call = 3; + SetRESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + optional google.protobuf.Duration timeout = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + optional string endpoint = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\""; + } + ]; +} + // Wait for response but response body is ignored, status is checked, call is sent as post. message SetRESTWebhook { @@ -27,39 +78,3 @@ message SetRESTCall { // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. message SetRESTAsync {} - -message Target { - // ID is the read-only unique identifier of the target. - string target_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"69629012906488334\""; - } - ]; - // Details provide some base information (such as the last change date) of the target. - zitadel.object.v2.Details details = 2; - - // Unique name of the target. - string name = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ip_allow_list\""; - } - ]; - // Defines the target type and how the response of the target is treated. - oneof target_type { - SetRESTWebhook rest_webhook = 4; - SetRESTCall rest_call = 5; - SetRESTAsync rest_async = 6; - } - // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 7 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"10s\""; - } - ]; - - string endpoint = 8 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"https://example.com/hooks/ip_check\""; - } - ]; -} \ No newline at end of file diff --git a/proto/zitadel/resources/object/v3alpha/object.proto b/proto/zitadel/resources/object/v3alpha/object.proto new file mode 100644 index 0000000000..65b3ef0c94 --- /dev/null +++ b/proto/zitadel/resources/object/v3alpha/object.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package zitadel.resources.object.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha;object"; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/object/v3alpha/object.proto"; + +message Details { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + + //sequence represents the order of events. It's always counting + // + // on read: the sequence of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + uint64 sequence = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + //change_date is the timestamp when the object was changed + // + // on read: the timestamp of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + google.protobuf.Timestamp change_date = 3; + //resource_owner represents the context an object belongs to + zitadel.object.v3alpha.Owner owner = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/settings/object/v3alpha/object.proto b/proto/zitadel/settings/object/v3alpha/object.proto new file mode 100644 index 0000000000..722db643d4 --- /dev/null +++ b/proto/zitadel/settings/object/v3alpha/object.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package zitadel.settings.object.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha;object"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/duration.proto"; + +import "zitadel/object/v3alpha/object.proto"; + +message Details { + //sequence represents the order of events. It's always counting + // + // on read: the sequence of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + uint64 sequence = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + //change_date is the timestamp when the object was changed + // + // on read: the timestamp of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + google.protobuf.Timestamp change_date = 2; + //resource_owner represents the context an object belongs to + zitadel.object.v3alpha.Owner owner = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} +