feat: add action v2 execution on requests and responses (#7637)

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: split request and response logic to handle the different context information

* feat: split request and response logic to handle the different context information

* fix: integration test

* fix: import alias

* fix: refactor execution package

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* docs: basic documentation for executions and targets

* fix: change order for interceptors

* fix: merge back origin/main

* fix: change target definition command and query side (#7735)

* fix: change target definition command and query side

* fix: correct refactoring name changes

* fix: correct refactoring name changes

* fix: changing execution defintion with target list and type

* fix: changing execution definition with target list and type

* fix: add back search queries for target and include

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* docs: add example to actions v2

* docs: add example to actions v2

* fix: correct integration tests on query for executions

* fix: add separate event for execution v2 as content changed

* fix: add separate event for execution v2 as content changed

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

---------

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>

* fix: added review comment changes

* fix: added review comment changes

* Update internal/api/grpc/server/middleware/execution_interceptor.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

---------

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Stefan Benz 2024-05-04 11:55:57 +02:00 committed by GitHub
parent 7e345444bf
commit 1c5ecba42a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 4397 additions and 1556 deletions

View File

@ -0,0 +1,139 @@
---
title: Actions v2 example execution locally
---
In this guide, you will create a ZITADEL execution and target. After a user is created through the API, the target is called.
## Prerequisites
Before you start, make sure you have everything set up correctly.
- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers)
- Your ZITADEL instance needs to have the actions feature enabled.
## Start example target
To start a simple HTTP server locally, which receives the webhook call, the following code example can be used:
```go
package main
import (
"fmt"
"io"
"net/http"
)
// webhook HandleFunc to read the request body and then print out the contents
func webhook(w http.ResponseWriter, req *http.Request) {
// read the body content
sentBody, err := io.ReadAll(req.Body)
if err != nil {
// if there was an error while reading the body return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
// print out the read content
fmt.Println(string(sentBody))
}
func main() {
// handle the HTTP call under "/webhook"
http.HandleFunc("/webhook", webhook)
// start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090"
http.ListenAndServe(":8090", nil)
}
```
What happens here is only a target which prints out the received request, which could also be handled with a different logic.
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:
[Create a target](/apis/resources/action_service_v3/action-service-create-target)
```shell
curl -L -X POST 'https://$CUSTOM-DOMAIN/v3alpha/targets' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"name": "local webhook",
"restWebhook": {
"interruptOnError": true
},
"endpoint": "http://localhost:8090/webhook",
"timeout": "10s"
}'
```
Save the returned ID to set in the execution.
## Set execution
To call the target just created before, with the intention to print the request used for user creation by the user V2 API, we define an execution with a method condition.
[Set an execution](/apis/resources/action_service_v3/action-service-set-execution)
```shell
curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"condition": {
"request": {
"method": "/zitadel.user.v2beta.UserService/AddHumanUser"
}
},
"targets": [
{
"target": "<TargetID returned>"
}
]
}'
```
## Example call
Now on every call on `/zitadel.user.v2beta.UserService/AddHumanUser` the local server prints out the received body of the request:
```shell
curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/users/human' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"profile": {
"givenName": "Example_given",
"familyName": "Example_family"
},
"email": {
"email": "example@example.com"
}
}'
```
Should print out something like, also described under [Sent information Request](./introduction#sent-information-request):
```shell
{
"fullMethod": "/zitadel.user.v2beta.UserService/AddHumanUser",
"instanceID": "262851882718855632",
"orgID": "262851882718921168",
"projectID": "262851882719052240",
"userID": "262851882718986704",
"request": {
"profile": {
"given_name": "Example_given",
"family_name": "Example_family"
},
"email": {
"email": "example@example.com"
}
}
}
```

View File

@ -0,0 +1,167 @@
---
title: Actions V2
---
This page describes the options you have when defining ZITADEL Actions V2.
## Endpoints
ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled.
### Sent information Request
The information sent to the Endpoint is structured as JSON:
```json
{
"fullMethod": "full method of the GRPC call",
"instanceID": "instanceID of the called instance",
"orgID": "ID of the organization related to the calling context",
"projectID": "ID of the project related to the used application",
"userID": "ID of the calling user",
"request": "full request of the call"
}
```
### Sent information Response
The information sent to the Endpoint is structured as JSON:
```json
{
"fullMethod": "full method of the GRPC call",
"instanceID": "instanceID of the called instance",
"orgID": "ID of the organization related to the calling context",
"projectID": "ID of the project related to the used application",
"userID": "ID of the calling user",
"request": "full request of the call",
"response": "full response of the call"
}
```
## Target
The Target describes how ZITADEL interacts with the Endpoint.
There are different types of Targets:
- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError
- `Call`, the call handles the status code and response, can be InterruptOnError
- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets
`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore.
The API documentation to create a target can be found [here](/apis/resources/action_service_v3/action-service-create-target)
## Execution
ZITADEL decides on specific conditions if one or more Targets have to be called.
The Execution resource contains 2 parts, the condition and the called targets.
The condition can be defined for 4 types of processes:
- `Requests`, before a request is processed by ZITADEL
- `Responses`, before a response is sent back to the application
- `Functions`, handling specific functionality in the logic of ZITADEL
- `Events`, after a specific event happened and was stored in ZITADEL
The API documentation to set an Execution can be found [here](/apis/resources/action_service_v3/action-service-set-execution)
### Condition Best Match
As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match.
This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2beta.UserService` and on `/zitadel.user.v2beta.UserService/AddHumanUser`,
ZITADEL would with a call on the `/zitadel.user.v2beta.UserService/AddHumanUser` use the Executions with the following priority:
1. `/zitadel.user.v2beta.UserService/AddHumanUser`
2. `zitadel.user.v2beta.UserService`
3. `all`
If you then have a call on `/zitadel.user.v2beta.UserService/UpdateHumanUser` the following priority would be found:
1. `zitadel.user.v2beta.UserService`
2. `all`
And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used.
### Targets and Includes
An execution can not only contain a list of Targets, but also Includes.
The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution.
If you define 2 Executions as follows:
```json
{
"condition": {
"request": {
"service": "zitadel.user.v2beta.UserService"
}
},
"targets": [
{
"target": "<TargetID1>"
}
]
}
```
```json
{
"condition": {
"request": {
"method": "/zitadel.user.v2beta.UserService/AddHumanUser"
}
},
"targets": [
{
"target": "<TargetID2>"
},
{
"include": {
"request": {
"service": "zitadel.user.v2beta.UserService"
}
}
}
]
}
```
The called Targets on "/zitadel.user.v2beta.UserService/AddHumanUser" would be, in order:
1. `<TargetID2>`
2. `<TargetID1>`
### Condition for Requests and Responses
For Request and Response there are 3 levels the condition can be defined:
- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API
- `Service`, handling any request or response under a service of the ZITADEL API
- `All`, handling any request or response under the ZITADEL API
The available conditions can be found under:
- [All available Methods](/apis/resources/action_service_v3/action-service-list-execution-methods), for example `/zitadel.user.v2beta.UserService/AddHumanUser`
- [All available Services](/apis/resources/action_service_v3/action-service-list-execution-services), for example `zitadel.user.v2beta.UserService`
### Condition for Functions
Replace the current Actions with the following flows:
- [Internal Authentication](../actions/internal-authentication)
- [External Authentication](../actions/external-authentication)
- [Complement Token](../actions/complement-token)
- [Customize SAML Response](../actions/customize-samlresponse)
The available conditions can be found under [all available Functions](/apis/resources/action_service_v3/action-service-list-execution-functions).
### Condition for Events
For event there are 3 levels the condition can be defined:
- Event, handling a specific event
- Group, handling a specific group of events
- All, handling any event in ZITADEL
The concept of events can be found under [Events](/concepts/architecture/software#events)

View File

@ -0,0 +1,40 @@
---
title: ZITADEL Actions v2
sidebar_label: Actions v2
---
By using ZITADEL actions V2, you can manipulate ZITADELs behavior on specific API calls, events or functions.
This is useful when you have special business requirements that ZITADEL doesn't support out-of-the-box.
:::info
We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features.
:::
## Why actions?
ZITADEL can't anticipate and solve every possible business rule and integration requirements from all ZITADEL users. Here are some examples:
- A business requires domain specific data validation before a user can be created or authenticated.
- A business needs to automate tasks. Roles should be assigned to users based on their ADFS 2016+ groups.
- A business needs to store metadata on a user that is used for integrating applications.
- A business needs to restrict the users who are allowed to register to a certain organization by their email domains.
With actions, ZITADEL provides a way to solve such problems.
## How it works
There are 3 components necessary:
- Endpoint, an external endpoint with the desired logic, can be whatever is necessary as long as it can receive an HTTP Post request.
- Target, a resource in ZITADEL with all necessary information how to trigger an endpoint
- Execution, a resource in ZITADEL with the information when to trigger which targets
The process is that ZITADEL decides at certain points that with a defined Execution a call to the defined Target(s) is triggered,
so that everybody can implement their custom behaviour for as many processes as possible.
Possible conditions for the Execution:
- Request, to react to or manipulate requests to ZITADEL, for example add information to newly created users
- Response, to react to or manipulate responses to ZITADEL, for example to provision newly created users to other systems
- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions)
- Event, to create to different events which get created in ZITADEL, for example to inform somebody if a user gets locked
## Further reading
- [Actions v2 example execution locally](/apis/actionsv2/execution-local)
- [Actions v2 reference](/apis/actionsv2/introduction#action)

View File

@ -795,6 +795,15 @@ module.exports = {
"apis/actions/objects",
],
},
{
type: "category",
label: "Actions V2",
collapsed: false,
items: [
"apis/actionsv2/introduction",
"apis/actionsv2/execution-local",
],
},
{
type: "doc",
label: "gRPC Status Codes",

View File

@ -7,6 +7,7 @@ import (
"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/repository/execution"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
@ -33,46 +34,46 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
return nil, err
}
targets := make([]*execution.Target, len(req.Targets))
for i, target := range req.Targets {
switch t := target.GetType().(type) {
case *action.ExecutionTargetType_Include:
include, err := conditionToInclude(t.Include)
if err != nil {
return nil, err
}
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include}
case *action.ExecutionTargetType_Target:
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target}
}
}
set := &command.SetExecution{
Targets: req.GetTargets(),
Includes: req.GetIncludes(),
Targets: targets,
}
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), set, authz.GetInstance(ctx).InstanceID())
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
@ -82,6 +83,36 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
}, nil
}
func conditionToInclude(cond *action.Condition) (string, error) {
switch t := cond.GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(), nil
case *action.Condition_Function:
cond := command.ExecutionFunctionCondition(t.Function.GetName())
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(), nil
}
return "", nil
}
func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutionRequest) (*action.DeleteExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
@ -91,37 +122,25 @@ func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutio
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
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 := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
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 := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
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), authz.GetInstance(ctx).InstanceID())
details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
@ -130,3 +149,27 @@ func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutio
Details: object.DomainToDetailsPb(details),
}, nil
}
func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: request.GetMethod(),
Service: request.GetService(),
All: request.GetAll(),
}
}
func executionConditionFromResponse(response *action.ResponseExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: response.GetMethod(),
Service: response.GetService(),
All: response.GetAll(),
}
}
func executionConditionFromEvent(event *action.EventExecution) *command.ExecutionEventCondition {
return &command.ExecutionEventCondition{
Event: event.GetEvent(),
Group: event.GetGroup(),
All: event.GetAll(),
}
}

View File

@ -9,14 +9,23 @@ import (
"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"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
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)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -48,7 +57,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
Request: &action.RequestExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -65,7 +74,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -82,7 +91,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -104,7 +113,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -121,7 +130,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -143,7 +152,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -163,17 +172,17 @@ func TestServer_SetExecution_Request(t *testing.T) {
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)
executionCond := "request"
Tester.SetExecution(CTX, t,
&action.Condition{
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
executionCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
@ -181,9 +190,10 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
},
[]string{targetResp.GetId()},
[]string{},
}
Tester.SetExecution(CTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetId()),
)
tests := []struct {
@ -206,7 +216,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Includes: []string{executionCond},
Targets: executionTargetsSingleInclude(executionCond),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -228,7 +238,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Includes: []string{executionCond},
Targets: executionTargetsSingleInclude(executionCond),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -237,6 +247,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
/* circular
{
name: "all, ok",
ctx: CTX,
@ -250,7 +261,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Includes: []string{executionCond},
Targets: executionTargetsSingleInclude(executionCond),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -259,6 +270,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -270,13 +282,16 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
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)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -332,7 +347,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
name: "method, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -373,7 +388,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
name: "service, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -398,7 +413,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
name: "all, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -441,7 +456,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
func TestServer_SetExecution_Response(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -473,7 +488,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -490,7 +505,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -507,7 +522,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -529,7 +544,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -546,7 +561,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -568,7 +583,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -588,13 +603,16 @@ func TestServer_SetExecution_Response(t *testing.T) {
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)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -652,7 +670,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
name: "method, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -693,7 +711,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
name: "service, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -718,7 +736,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
name: "all, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -761,7 +779,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
func TestServer_SetExecution_Event(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -795,7 +813,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
Event: &action.EventExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -833,7 +851,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -876,7 +894,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -898,7 +916,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -918,13 +936,16 @@ func TestServer_SetExecution_Event(t *testing.T) {
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)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -985,7 +1006,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
name: "event, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -1026,7 +1047,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
name: "group, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -1061,18 +1082,13 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
},
},
},
want: &action.DeleteExecutionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantErr: true,
},
{
name: "all, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@ -1115,7 +1131,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
func TestServer_SetExecution_Function(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -1147,7 +1163,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -1157,10 +1173,10 @@ func TestServer_SetExecution_Function(t *testing.T) {
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "xxx",
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@ -1170,10 +1186,10 @@ func TestServer_SetExecution_Function(t *testing.T) {
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication",
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@ -1193,13 +1209,16 @@ func TestServer_SetExecution_Function(t *testing.T) {
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)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@ -1243,7 +1262,7 @@ func TestServer_DeleteExecution_Function(t *testing.T) {
req: &action.DeleteExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "xxx",
Function: &action.FunctionExecution{Name: "xxx"},
},
},
},
@ -1253,13 +1272,13 @@ func TestServer_DeleteExecution_Function(t *testing.T) {
name: "function, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication",
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
},
},
},

View File

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

View File

@ -2,6 +2,7 @@ package action
import (
"context"
"strings"
"google.golang.org/protobuf/types/known/durationpb"
@ -67,8 +68,6 @@ func targetFieldNameToSortingColumn(field action.TargetFieldName) query.Column {
return query.TargetColumnURL
case action.TargetFieldName_FIELD_NAME_TIMEOUT:
return query.TargetColumnTimeout
case action.TargetFieldName_FIELD_NAME_ASYNC:
return query.TargetColumnAsync
case action.TargetFieldName_FIELD_NAME_INTERRUPT_ON_ERROR:
return query.TargetColumnInterruptOnError
default:
@ -134,19 +133,16 @@ func targetToPb(t *query.Target) *action.Target {
TargetId: t.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
}
if t.Async {
target.ExecutionType = &action.Target_IsAsync{IsAsync: t.Async}
}
if t.InterruptOnError {
target.ExecutionType = &action.Target_InterruptOnError{InterruptOnError: t.InterruptOnError}
Endpoint: t.Endpoint,
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{Url: t.URL}}
case domain.TargetTypeRequestResponse:
target.TargetType = &action.Target_RestRequestResponse{RestRequestResponse: &action.SetRESTRequestResponse{Url: t.URL}}
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
}
@ -205,10 +201,14 @@ func executionQueryToQuery(searchQuery *action.SearchQuery) (query.SearchQuery,
return inConditionsQueryToQuery(q.InConditionsQuery)
case *action.SearchQuery_ExecutionTypeQuery:
return executionTypeToQuery(q.ExecutionTypeQuery)
case *action.SearchQuery_TargetQuery:
return query.NewExecutionTargetSearchQuery(q.TargetQuery.GetTargetId())
case *action.SearchQuery_IncludeQuery:
return query.NewExecutionIncludeSearchQuery(q.IncludeQuery.GetInclude())
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")
}
@ -267,7 +267,7 @@ func conditionToID(q *action.Condition) (string, error) {
}
return cond.ID(), nil
case *action.Condition_Function:
return t.Function, nil
return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
@ -282,17 +282,83 @@ func executionsToPb(executions []*query.Execution) []*action.Execution {
}
func executionToPb(e *query.Execution) *action.Execution {
var targets, includes []string
if len(e.Targets) > 0 {
targets = e.Targets
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
}
if len(e.Includes) > 0 {
includes = e.Includes
}
return &action.Execution{
Details: object.DomainToDetailsPb(&e.ObjectDetails),
ExecutionId: e.ID,
Condition: executionIDToCondition(e.ID),
Targets: targets,
Includes: includes,
}
}
func executionIDToCondition(include string) *action.Condition {
if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) {
return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) {
return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) {
return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) {
return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String()))
}
return nil
}
func includeRequestToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 2:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}}
case 1:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case 0:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}
default:
return nil
}
}
func includeResponseToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 2:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}}
case 1:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case 0:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}
default:
return nil
}
}
func includeEventToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 1:
if strings.HasSuffix(id, command.EventGroupSuffix) {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}}
} else {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}}
}
case 0:
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}
default:
return nil
}
}
func includeFunctionToCondition(id string) *action.Condition {
return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}}
}

View File

@ -5,6 +5,7 @@ package action_test
import (
"context"
"fmt"
"reflect"
"testing"
"time"
@ -12,6 +13,7 @@ import (
"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"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@ -52,7 +54,7 @@ func TestServer_GetTargetByID(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
@ -69,10 +71,9 @@ func TestServer_GetTargetByID(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
},
@ -84,7 +85,7 @@ func TestServer_GetTargetByID(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, true, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
@ -101,23 +102,21 @@ func TestServer_GetTargetByID(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_IsAsync{IsAsync: true},
},
},
},
{
name: "get, interruptOnError, ok",
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.CreateTargetWithNameAndType(ctx, t, name, false, true)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
@ -134,13 +133,79 @@ func TestServer_GetTargetByID(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
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),
ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true},
},
},
},
@ -163,15 +228,11 @@ func TestServer_GetTargetByID(t *testing.T) {
assert.Error(ttt, getErr, "Error: "+getErr.Error())
} else {
assert.NoError(ttt, getErr)
}
if getErr != nil {
fmt.Println("Error: " + getErr.Error())
return
}
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")
})
@ -227,14 +288,14 @@ func TestServer_ListTargets(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false)
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.Details.ProcessedSequence = resp.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
@ -255,9 +316,10 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
@ -270,7 +332,7 @@ func TestServer_ListTargets(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Queries[0].Query = &action.TargetSearchQuery_TargetNameQuery{
TargetNameQuery: &action.TargetNameQuery{
TargetName: name,
@ -298,9 +360,10 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
@ -316,9 +379,9 @@ func TestServer_ListTargets(t *testing.T) {
name1 := fmt.Sprint(time.Now().UnixNano() + 1)
name2 := fmt.Sprint(time.Now().UnixNano() + 3)
name3 := fmt.Sprint(time.Now().UnixNano() + 5)
resp1 := Tester.CreateTargetWithNameAndType(ctx, t, name1, false, false)
resp2 := Tester.CreateTargetWithNameAndType(ctx, t, name2, true, false)
resp3 := Tester.CreateTargetWithNameAndType(ctx, t, name3, false, true)
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()},
@ -354,9 +417,10 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
@ -365,25 +429,23 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_IsAsync{IsAsync: true},
},
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true},
},
},
},
@ -422,9 +484,9 @@ func TestServer_ListTargets(t *testing.T) {
}
}
func TestServer_ListExecutions_Request(t *testing.T) {
func TestServer_ListExecutions(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
ctx context.Context
@ -446,17 +508,20 @@ func TestServer_ListExecutions_Request(t *testing.T) {
wantErr: true,
},
{
name: "list single condition",
name: "list request single condition",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
resp := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{})
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()
// 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{
@ -471,8 +536,7 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
},
},
}},
},
},
}},
@ -487,18 +551,26 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession",
Targets: []string{targetResp.GetId()},
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
},
},
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
},
},
},
{
name: "list single target",
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)
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{
@ -507,7 +579,7 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
}
resp := Tester.SetExecution(ctx, t, &action.Condition{
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
@ -515,14 +587,17 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
}, []string{target.GetId()}, []string{})
}
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].Targets[0] = target.GetId()
response.Result[0].Condition = cond
response.Result[0].Targets = targets
return nil
},
req: &action.ListExecutionsRequest{
@ -538,17 +613,17 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.management.v1.ManagementService/UpdateAction",
Targets: []string{""},
Condition: &action.Condition{},
Targets: executionTargetsSingleTarget(""),
},
},
},
}, {
name: "list single include",
name: "list request single include",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
Tester.SetExecution(ctx, t, &action.Condition{
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
@ -556,8 +631,11 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
}, []string{targetResp.GetId()}, []string{})
resp2 := Tester.SetExecution(ctx, t, &action.Condition{
}
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{
@ -565,19 +643,23 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
}, []string{}, []string{"request./zitadel.management.v1.ManagementService/GetAction"})
}
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{Include: "request./zitadel.management.v1.ManagementService/GetAction"},
IncludeQuery: &action.IncludeQuery{},
},
}},
},
@ -591,8 +673,6 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.management.v1.ManagementService/ListActions",
Includes: []string{"request./zitadel.management.v1.ManagementService/GetAction"},
},
},
},
@ -603,19 +683,32 @@ func TestServer_ListExecutions_Request(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
resp1 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{})
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
resp2 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[1], []string{targetResp.GetId()}, []string{})
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
resp3 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[2], []string{targetResp.GetId()}, []string{})
response.Details.Timestamp = resp3.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp3.GetDetails().GetSequence()
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{
@ -665,24 +758,77 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession",
Targets: []string{targetResp.GetId()},
}, {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/CreateSession",
Targets: []string{targetResp.GetId()},
}, {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/SetSession",
Targets: []string{targetResp.GetId()},
},
},
},
},
{
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.v2beta.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2beta.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.v2beta.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2beta.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) {
@ -699,20 +845,33 @@ func TestServer_ListExecutions_Request(t *testing.T) {
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, listErr, "Error: "+listErr.Error())
assert.Error(t, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(ttt, listErr)
assert.NoError(t, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
assert.Len(t, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
// as not sorted, all elements have to be checked
// workaround as oneof elements can only be checked with assert.EqualExportedValues()
if j, found := containExecution(got.Result, tt.want.Result[i]); found {
assert.EqualExportedValues(t, tt.want.Result[i], got.Result[j])
}
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func containExecution(executionList []*action.Execution, execution *action.Execution) (int, bool) {
for i, exec := range executionList {
if reflect.DeepEqual(exec.Details, execution.Details) {
return i, true
}
}
return 0, false
}

View File

@ -66,5 +66,5 @@ func checkExecutionEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().Actions {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-141bwx3lef", "Errors.action.NotEnabled")
return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled")
}

View File

@ -58,23 +58,26 @@ func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetReque
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
var targetType domain.TargetType
var url string
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := req.GetTargetType().(type) {
case *action.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
url = t.RestWebhook.GetUrl()
case *action.CreateTargetRequest_RestRequestResponse:
targetType = domain.TargetTypeRequestResponse
url = t.RestRequestResponse.GetUrl()
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,
URL: url,
Endpoint: req.GetEndpoint(),
Timeout: req.GetTimeout().AsDuration(),
Async: req.GetIsAsync(),
InterruptOnError: req.GetInterruptOnError(),
InterruptOnError: interruptOnError,
}
}
@ -87,21 +90,23 @@ func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarge
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.URL = gu.Ptr(t.RestWebhook.GetUrl())
case *action.UpdateTargetRequest_RestRequestResponse:
target.TargetType = gu.Ptr(domain.TargetTypeRequestResponse)
target.URL = gu.Ptr(t.RestRequestResponse.GetUrl())
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())
}
if req.ExecutionType != nil {
target.Async = gu.Ptr(req.GetIsAsync())
target.InterruptOnError = gu.Ptr(req.GetInterruptOnError())
}
return target
}

View File

@ -14,6 +14,7 @@ import (
"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"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@ -69,8 +70,8 @@ func TestServer_CreateTarget(t *testing.T) {
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{},
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
@ -80,28 +81,24 @@ func TestServer_CreateTarget(t *testing.T) {
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
ExecutionType: nil,
},
wantErr: true,
},
{
name: "empty execution type, ok",
name: "async, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: nil,
},
want: &action.CreateTargetResponse{
Details: &object.Details{
@ -111,19 +108,17 @@ func TestServer_CreateTarget(t *testing.T) {
},
},
{
name: "async execution, ok",
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{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_IsAsync{
IsAsync: true,
},
},
want: &action.CreateTargetResponse{
Details: &object.Details{
@ -133,20 +128,59 @@ func TestServer_CreateTarget(t *testing.T) {
},
},
{
name: "interrupt on error execution, ok",
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{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_InterruptOnError{
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(),
@ -186,7 +220,7 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
@ -215,7 +249,7 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
@ -235,16 +269,16 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
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_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com",
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
@ -259,18 +293,14 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
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_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/new",
},
},
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
want: &action.UpdateTargetResponse{
@ -283,7 +313,7 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
@ -301,17 +331,17 @@ func TestServer_UpdateTarget(t *testing.T) {
},
},
{
name: "change execution type, ok",
name: "change type async, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
ExecutionType: &action.UpdateTargetRequest_IsAsync{
IsAsync: true,
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
@ -341,7 +371,7 @@ func TestServer_UpdateTarget(t *testing.T) {
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context

View File

@ -27,32 +27,44 @@ func Test_createTargetToCommand(t *testing.T) {
args: args{nil},
want: &command.AddTarget{
Name: "",
URL: "",
Endpoint: "",
Timeout: 0,
Async: false,
InterruptOnError: false,
},
},
{
name: "all fields (async webhook)",
name: "all fields (webhook)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/1",
},
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_IsAsync{
IsAsync: true,
},
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
URL: "https://example.com/hooks/1",
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (async)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeAsync,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
Async: true,
InterruptOnError: false,
},
},
@ -60,22 +72,19 @@ func Test_createTargetToCommand(t *testing.T) {
name: "all fields (interrupting response)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
TargetType: &action.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com/hooks/1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeRequestResponse,
URL: "https://example.com/hooks/1",
TargetType: domain.TargetTypeCall,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
Async: false,
InterruptOnError: true,
},
},
@ -108,14 +117,12 @@ func Test_updateTargetToCommand(t *testing.T) {
Name: nil,
TargetType: nil,
Timeout: nil,
ExecutionType: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
URL: nil,
Endpoint: nil,
Timeout: nil,
Async: nil,
InterruptOnError: nil,
},
},
@ -125,37 +132,70 @@ func Test_updateTargetToCommand(t *testing.T) {
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
ExecutionType: nil,
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
URL: nil,
Endpoint: nil,
Timeout: gu.Ptr(0 * time.Second),
Async: nil,
InterruptOnError: nil,
},
},
{
name: "all fields (async webhook)",
name: "all fields (webhook)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/1",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.UpdateTargetRequest_IsAsync{
IsAsync: true,
},
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
URL: gu.Ptr("https://example.com/hooks/1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (webhook interrupt)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
{
name: "all fields (async)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeAsync),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(true),
InterruptOnError: gu.Ptr(false),
},
},
@ -163,22 +203,19 @@ func Test_updateTargetToCommand(t *testing.T) {
name: "all fields (interrupting response)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
TargetType: &action.UpdateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com/hooks/1",
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.UpdateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeRequestResponse),
URL: gu.Ptr("https://example.com/hooks/1"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(false),
InterruptOnError: gu.Ptr(true),
},
},

View File

@ -0,0 +1,179 @@
package middleware
import (
"context"
"encoding/json"
"strings"
"github.com/zitadel/logging"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
"github.com/zitadel/zitadel/internal/query"
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
requestTargets, responseTargets := queryTargets(ctx, queries, info.FullMethod)
// call targets otherwise return req
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req)
if err != nil {
return nil, err
}
response, err := handler(ctx, handledReq)
if err != nil {
return nil, err
}
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response)
}
}
func executeTargetsForRequest(ctx context.Context, targets []execution.Target, fullMethod string, req interface{}) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
// if no targets are found, return without any calls
if len(targets) == 0 {
return req, nil
}
ctxData := authz.GetCtxData(ctx)
info := &ContextInfoRequest{
FullMethod: fullMethod,
InstanceID: authz.GetInstance(ctx).InstanceID(),
ProjectID: ctxData.ProjectID,
OrgID: ctxData.OrgID,
UserID: ctxData.UserID,
Request: req,
}
return execution.CallTargets(ctx, targets, info)
}
func executeTargetsForResponse(ctx context.Context, targets []execution.Target, fullMethod string, req, resp interface{}) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
// if no targets are found, return without any calls
if len(targets) == 0 {
return resp, nil
}
ctxData := authz.GetCtxData(ctx)
info := &ContextInfoResponse{
FullMethod: fullMethod,
InstanceID: authz.GetInstance(ctx).InstanceID(),
ProjectID: ctxData.ProjectID,
OrgID: ctxData.OrgID,
UserID: ctxData.UserID,
Request: req,
Response: resp,
}
return execution.CallTargets(ctx, targets, info)
}
type ExecutionQueries interface {
TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error)
}
func queryTargets(
ctx context.Context,
queries ExecutionQueries,
fullMethod string,
) ([]execution.Target, []execution.Target) {
ctx, span := tracing.NewSpan(ctx)
defer span.End()
targets, err := queries.TargetsByExecutionIDs(ctx,
idsForFullMethod(fullMethod, domain.ExecutionTypeRequest),
idsForFullMethod(fullMethod, domain.ExecutionTypeResponse),
)
requestTargets := make([]execution.Target, 0, len(targets))
responseTargets := make([]execution.Target, 0, len(targets))
if err != nil {
logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets")
return requestTargets, responseTargets
}
for _, target := range targets {
if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeRequest)) {
requestTargets = append(requestTargets, target)
} else if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeResponse)) {
responseTargets = append(responseTargets, target)
}
}
return requestTargets, responseTargets
}
func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string {
return []string{exec_repo.ID(executionType, fullMethod), exec_repo.ID(executionType, serviceFromFullMethod(fullMethod)), exec_repo.IDAll(executionType)}
}
func serviceFromFullMethod(s string) string {
parts := strings.Split(s, "/")
return parts[1]
}
var _ execution.ContextInfo = &ContextInfoRequest{}
type ContextInfoRequest struct {
FullMethod string `json:"fullMethod,omitempty"`
InstanceID string `json:"instanceID,omitempty"`
OrgID string `json:"orgID,omitempty"`
ProjectID string `json:"projectID,omitempty"`
UserID string `json:"userID,omitempty"`
Request interface{} `json:"request,omitempty"`
}
func (c *ContextInfoRequest) GetHTTPRequestBody() []byte {
data, err := json.Marshal(c)
if err != nil {
return nil
}
return data
}
func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error {
return json.Unmarshal(resp, c.Request)
}
func (c *ContextInfoRequest) GetContent() interface{} {
return c.Request
}
var _ execution.ContextInfo = &ContextInfoResponse{}
type ContextInfoResponse struct {
FullMethod string `json:"fullMethod,omitempty"`
InstanceID string `json:"instanceID,omitempty"`
OrgID string `json:"orgID,omitempty"`
ProjectID string `json:"projectID,omitempty"`
UserID string `json:"userID,omitempty"`
Request interface{} `json:"request,omitempty"`
Response interface{} `json:"response,omitempty"`
}
func (c *ContextInfoResponse) GetHTTPRequestBody() []byte {
data, err := json.Marshal(c)
if err != nil {
return nil
}
return data
}
func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error {
return json.Unmarshal(resp, c.Response)
}
func (c *ContextInfoResponse) GetContent() interface{} {
return c.Response
}

View File

@ -0,0 +1,778 @@
package middleware
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
)
var _ execution.Target = &mockExecutionTarget{}
type mockExecutionTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
e.Endpoint = endpoint
}
func (e *mockExecutionTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *mockExecutionTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *mockExecutionTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *mockExecutionTarget) GetTimeout() time.Duration {
return e.Timeout
}
func (e *mockExecutionTarget) GetTargetID() string {
return e.TargetID
}
func (e *mockExecutionTarget) GetExecutionID() string {
return e.ExecutionID
}
type mockContentRequest struct {
Content string
}
func newMockContentRequest(content string) *mockContentRequest {
return &mockContentRequest{
Content: content,
}
}
func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest {
return &ContextInfoRequest{
FullMethod: fullMethod,
Request: newMockContentRequest(request),
}
}
func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse {
return &ContextInfoResponse{
FullMethod: fullMethod,
Request: newMockContentRequest(request),
Response: newMockContentRequest(response),
}
}
func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
type target struct {
reqBody execution.ContextInfo
sleep time.Duration
statusCode int
respBody interface{}
}
type args struct {
ctx context.Context
executionTargets []execution.Target
targets []target
fullMethod string
req interface{}
}
type res struct {
want interface{}
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"target, executionTargets nil",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: nil,
req: newMockContentRequest("request"),
},
res{
want: newMockContentRequest("request"),
},
},
{
"target, executionTargets empty",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{},
req: newMockContentRequest("request"),
},
res{
want: newMockContentRequest("request"),
},
},
{
"target, not reachable",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, error without interrupt",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusBadRequest,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"target, interruptOnError",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusBadRequest,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, wrong request",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{reqBody: newMockContextInfoRequest("/service/method", "wrong")},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content1"),
},
},
{
"target async, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Second,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"target async, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Minute,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"webhook, error",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
sleep: 0,
statusCode: http.StatusInternalServerError,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"webhook, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"webhook, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"with includes, interruptOnError",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target1",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target2",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target3",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content1"),
respBody: newMockContentRequest("content2"),
sleep: 0,
statusCode: http.StatusBadRequest,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content2"),
respBody: newMockContentRequest("content3"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"with includes, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target1",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target2",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target3",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content1"),
respBody: newMockContentRequest("content2"),
sleep: 5 * time.Second,
statusCode: http.StatusBadRequest,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content2"),
respBody: newMockContentRequest("content3"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
closeFuncs := make([]func(), len(tt.args.targets))
for i, target := range tt.args.targets {
url, closeF := testServerCall(
target.reqBody,
target.sleep,
target.statusCode,
target.respBody,
)
et := tt.args.executionTargets[i].(*mockExecutionTarget)
et.SetEndpoint(url)
closeFuncs[i] = closeF
}
resp, err := executeTargetsForRequest(
tt.args.ctx,
tt.args.executionTargets,
tt.args.fullMethod,
tt.args.req,
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.want, resp)
for _, closeF := range closeFuncs {
closeF()
}
})
}
}
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", http.StatusInternalServerError)
return
}
sentBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if !reflect.DeepEqual(data, sentBody) {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if statusCode != http.StatusOK {
http.Error(w, "error", 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
}
func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
type target struct {
reqBody execution.ContextInfo
sleep time.Duration
statusCode int
respBody interface{}
}
type args struct {
ctx context.Context
executionTargets []execution.Target
targets []target
fullMethod string
req interface{}
resp interface{}
}
type res struct {
want interface{}
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"target, executionTargets nil",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: nil,
req: newMockContentRequest("request"),
resp: newMockContentRequest("response"),
},
res{
want: newMockContentRequest("response"),
},
},
{
"target, executionTargets empty",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{},
req: newMockContentRequest("request"),
resp: newMockContentRequest("response"),
},
res{
want: newMockContentRequest("response"),
},
},
{
"target, empty response",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest(""),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: []byte{},
},
res{
wantErr: true,
},
},
{
"target, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoResponse("/service/method", "request", "response"),
respBody: newMockContentRequest("response1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("request"),
resp: newMockContentRequest("response"),
},
res{
want: newMockContentRequest("response1"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
closeFuncs := make([]func(), len(tt.args.targets))
for i, target := range tt.args.targets {
url, closeF := testServerCall(
target.reqBody,
target.sleep,
target.statusCode,
target.respBody,
)
et := tt.args.executionTargets[i].(*mockExecutionTarget)
et.SetEndpoint(url)
closeFuncs[i] = closeF
}
resp, err := executeTargetsForResponse(
tt.args.ctx,
tt.args.executionTargets,
tt.args.fullMethod,
tt.args.req,
tt.args.resp,
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.want, resp)
for _, closeF := range closeFuncs {
closeF()
}
})
}
}

View File

@ -58,6 +58,7 @@ func CreateServer(
middleware.AuthorizationInterceptor(verifier, authConfig),
middleware.TranslationHandler(),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
middleware.ExecutionHandler(queries),
middleware.ValidationHandler(),
middleware.ServiceHandler(),
middleware.ActivityInterceptor(),

View File

@ -2,6 +2,7 @@ package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@ -9,6 +10,10 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
EventGroupSuffix = ".*"
)
type ExecutionAPICondition struct {
Method string
Service string
@ -134,7 +139,11 @@ func (e *ExecutionEventCondition) ID() string {
return execution.ID(domain.ExecutionTypeEvent, e.Event)
}
if e.Group != "" {
return execution.ID(domain.ExecutionTypeEvent, e.Group)
group := e.Group
if !strings.HasSuffix(e.Group, EventGroupSuffix) {
group += EventGroupSuffix
}
return execution.ID(domain.ExecutionTypeEvent, group)
}
if e.All {
return execution.IDAll(domain.ExecutionTypeEvent)
@ -168,25 +177,43 @@ func (c *Commands) SetExecutionEvent(ctx context.Context, cond *ExecutionEventCo
type SetExecution struct {
models.ObjectRoot
Targets []string
Includes []string
Targets []*execution.Target
}
func (t SetExecution) GetIncludes() []string {
includes := make([]string, 0)
for i := range t.Targets {
if t.Targets[i].Type == domain.ExecutionTargetTypeInclude {
includes = append(includes, t.Targets[i].Target)
}
}
return includes
}
func (t SetExecution) GetTargets() []string {
targets := make([]string, 0)
for i := range t.Targets {
if t.Targets[i].Type == domain.ExecutionTargetTypeTarget {
targets = append(targets, t.Targets[i].Target)
}
}
return targets
}
func (e *SetExecution) IsValid() error {
if len(e.Targets) == 0 && len(e.Includes) == 0 {
if len(e.Targets) == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-56bteot2uj", "Errors.Execution.NoTargets")
}
if len(e.Targets) > 0 && len(e.Includes) > 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-5zleae34r1", "Errors.Execution.Invalid")
}
return nil
}
func (e *SetExecution) Existing(c *Commands, ctx context.Context, resourceOwner string) error {
if len(e.Targets) > 0 && !c.existsTargetsByIDs(ctx, e.Targets, resourceOwner) {
targets := e.GetTargets()
if len(targets) > 0 && !c.existsTargetsByIDs(ctx, targets, resourceOwner) {
return zerrors.ThrowNotFound(nil, "COMMAND-17e8fq1ggk", "Errors.Target.NotFound")
}
if len(e.Includes) > 0 && !c.existsExecutionsByIDs(ctx, e.Includes, resourceOwner) {
includes := e.GetIncludes()
if len(includes) > 0 && !c.existsExecutionsByIDs(ctx, includes, resourceOwner) {
return zerrors.ThrowNotFound(nil, "COMMAND-slgj0l4cdz", "Errors.Execution.IncludeNotFound")
}
return nil
@ -206,11 +233,10 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource
return nil, err
}
if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEvent(
if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEventV2(
ctx,
ExecutionAggregateFromWriteModel(&wm.WriteModel),
set.Targets,
set.Includes,
)); err != nil {
return nil, err
}

View File

@ -12,10 +12,11 @@ type ExecutionWriteModel struct {
Targets []string
Includes []string
ExecutionTargets []*execution.Target
}
func (e *ExecutionWriteModel) Exists() bool {
return len(e.Targets) > 0 || len(e.Includes) > 0
return len(e.ExecutionTargets) > 0 || len(e.Includes) > 0 || len(e.Targets) > 0
}
func NewExecutionWriteModel(id string, resourceOwner string) *ExecutionWriteModel {
@ -34,9 +35,12 @@ func (wm *ExecutionWriteModel) Reduce() error {
case *execution.SetEvent:
wm.Targets = e.Targets
wm.Includes = e.Includes
case *execution.SetEventV2:
wm.ExecutionTargets = e.Targets
case *execution.RemovedEvent:
wm.Targets = nil
wm.Includes = nil
wm.ExecutionTargets = nil
}
}
return wm.WriteModel.Reduce()
@ -49,6 +53,7 @@ func (wm *ExecutionWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(execution.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(execution.SetEventType,
execution.SetEventV2Type,
execution.RemovedEventType).
Builder()
}
@ -91,6 +96,10 @@ func (wm *ExecutionsExistWriteModel) Reduce() error {
if !slices.Contains(wm.existingIDs, e.Aggregate().ID) {
wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID)
}
case *execution.SetEventV2:
if !slices.Contains(wm.existingIDs, e.Aggregate().ID) {
wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID)
}
case *execution.RemovedEvent:
i := slices.Index(wm.existingIDs, e.Aggregate().ID)
if i >= 0 {
@ -108,6 +117,7 @@ func (wm *ExecutionsExistWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(execution.AggregateType).
AggregateIDs(wm.ids...).
EventTypes(execution.SetEventType,
execution.SetEventV2Type,
execution.RemovedEventType).
Builder()
}

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/execution"
)
@ -32,10 +33,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -53,10 +56,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -65,10 +70,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -91,10 +98,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -112,10 +121,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -138,24 +149,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -174,17 +191,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -193,10 +214,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -214,17 +237,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -233,10 +260,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -254,24 +283,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -299,24 +334,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@ -334,10 +375,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -346,17 +389,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -385,10 +432,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@ -397,17 +446,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,8 @@ type AddTarget struct {
Name string
TargetType domain.TargetType
URL string
Endpoint string
Timeout time.Duration
Async bool
InterruptOnError bool
}
@ -29,9 +28,9 @@ func (a *AddTarget) IsValid() error {
if a.Timeout == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-39f35d8uri", "Errors.Target.NoTimeout")
}
_, err := url.Parse(a.URL)
if err != nil || a.URL == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
_, err := url.Parse(a.Endpoint)
if err != nil || a.Endpoint == "" {
return zerrors.ThrowInvalidArgument(err, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
}
return nil
@ -65,9 +64,8 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
TargetAggregateFromWriteModel(&wm.WriteModel),
add.Name,
add.TargetType,
add.URL,
add.Endpoint,
add.Timeout,
add.Async,
add.InterruptOnError,
))
if err != nil {
@ -84,9 +82,8 @@ type ChangeTarget struct {
Name *string
TargetType *domain.TargetType
URL *string
Endpoint *string
Timeout *time.Duration
Async *bool
InterruptOnError *bool
}
@ -100,10 +97,10 @@ func (a *ChangeTarget) IsValid() error {
if a.Timeout != nil && *a.Timeout == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-08b39vdi57", "Errors.Target.NoTimeout")
}
if a.URL != nil {
_, err := url.Parse(*a.URL)
if err != nil || *a.URL == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
if a.Endpoint != nil {
_, err := url.Parse(*a.Endpoint)
if err != nil || *a.Endpoint == "" {
return zerrors.ThrowInvalidArgument(err, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
}
}
return nil
@ -130,9 +127,8 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
TargetAggregateFromWriteModel(&existing.WriteModel),
change.Name,
change.TargetType,
change.URL,
change.Endpoint,
change.Timeout,
change.Async,
change.InterruptOnError)
if changedEvent == nil {
return writeModelToObjectDetails(&existing.WriteModel), nil

View File

@ -15,9 +15,8 @@ type TargetWriteModel struct {
Name string
TargetType domain.TargetType
URL string
Endpoint string
Timeout time.Duration
Async bool
InterruptOnError bool
State domain.TargetState
@ -39,9 +38,8 @@ func (wm *TargetWriteModel) Reduce() error {
case *target.AddedEvent:
wm.Name = e.Name
wm.TargetType = e.TargetType
wm.URL = e.URL
wm.Endpoint = e.Endpoint
wm.Timeout = e.Timeout
wm.Async = e.Async
wm.State = domain.TargetActive
case *target.ChangedEvent:
if e.Name != nil {
@ -50,15 +48,12 @@ func (wm *TargetWriteModel) Reduce() error {
if e.TargetType != nil {
wm.TargetType = *e.TargetType
}
if e.URL != nil {
wm.URL = *e.URL
if e.Endpoint != nil {
wm.Endpoint = *e.Endpoint
}
if e.Timeout != nil {
wm.Timeout = *e.Timeout
}
if e.Async != nil {
wm.Async = *e.Async
}
if e.InterruptOnError != nil {
wm.InterruptOnError = *e.InterruptOnError
}
@ -86,9 +81,8 @@ func (wm *TargetWriteModel) NewChangedEvent(
agg *eventstore.Aggregate,
name *string,
targetType *domain.TargetType,
url *string,
endpoint *string,
timeout *time.Duration,
async *bool,
interruptOnError *bool,
) *target.ChangedEvent {
changes := make([]target.Changes, 0)
@ -98,15 +92,12 @@ func (wm *TargetWriteModel) NewChangedEvent(
if targetType != nil && wm.TargetType != *targetType {
changes = append(changes, target.ChangeTargetType(*targetType))
}
if url != nil && wm.URL != *url {
changes = append(changes, target.ChangeURL(*url))
if endpoint != nil && wm.Endpoint != *endpoint {
changes = append(changes, target.ChangeEndpoint(*endpoint))
}
if timeout != nil && wm.Timeout != *timeout {
changes = append(changes, target.ChangeTimeout(*timeout))
}
if async != nil && wm.Async != *async {
changes = append(changes, target.ChangeAsync(*async))
}
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
changes = append(changes, target.ChangeInterruptOnError(*interruptOnError))
}

View File

@ -20,7 +20,6 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
"https://example.com",
time.Second,
false,
false,
)
}

View File

@ -19,7 +19,7 @@ import (
func TestCommands_AddTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@ -41,7 +41,7 @@ func TestCommands_AddTarget(t *testing.T) {
{
"no resourceowner, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
@ -55,12 +55,12 @@ func TestCommands_AddTarget(t *testing.T) {
{
"no name, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@ -69,50 +69,50 @@ func TestCommands_AddTarget(t *testing.T) {
{
"no timeout, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no url, error",
"no Endpoint, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
Timeout: time.Second,
URL: "",
Endpoint: "",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no parsable url, error",
"no parsable Endpoint, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
Timeout: time.Second,
URL: "://",
Endpoint: "://",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@ -121,18 +121,17 @@ func TestCommands_AddTarget(t *testing.T) {
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
),
),
),
@ -142,11 +141,11 @@ func TestCommands_AddTarget(t *testing.T) {
ctx: context.Background(),
add: &AddTarget{
Name: "name",
URL: "https://example.com",
Endpoint: "https://example.com",
Timeout: time.Second,
TargetType: domain.TargetTypeWebhook,
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsPreconditionFailed,
@ -155,16 +154,10 @@ func TestCommands_AddTarget(t *testing.T) {
{
"already existing",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
eventFromEventPusher(
targetAddEvent("target", "instance"),
),
),
),
@ -176,9 +169,9 @@ func TestCommands_AddTarget(t *testing.T) {
Name: "name",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
URL: "https://example.com",
Endpoint: "https://example.com",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorAlreadyExists,
@ -187,18 +180,10 @@ func TestCommands_AddTarget(t *testing.T) {
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
idGenerator: mock.ExpectID(t, "id1"),
@ -209,32 +194,28 @@ func TestCommands_AddTarget(t *testing.T) {
Name: "name",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
URL: "https://example.com",
Endpoint: "https://example.com",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
{
"push full ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
true,
true,
),
func() eventstore.Command {
event := targetAddEvent("id1", "instance")
event.InterruptOnError = true
return event
}(),
),
),
idGenerator: mock.ExpectID(t, "id1"),
@ -244,17 +225,16 @@ func TestCommands_AddTarget(t *testing.T) {
add: &AddTarget{
Name: "name",
TargetType: domain.TargetTypeWebhook,
URL: "https://example.com",
Endpoint: "https://example.com",
Timeout: time.Second,
Async: true,
InterruptOnError: true,
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
@ -262,7 +242,7 @@ func TestCommands_AddTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
@ -282,7 +262,7 @@ func TestCommands_AddTarget(t *testing.T) {
func TestCommands_ChangeTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -302,7 +282,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"resourceowner missing, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
@ -316,12 +296,12 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@ -330,14 +310,14 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"name empty, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
Name: gu.Ptr(""),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@ -346,46 +326,46 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"timeout empty, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
Timeout: gu.Ptr(time.Duration(0)),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"url empty, error",
"Endpoint empty, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
URL: gu.Ptr(""),
Endpoint: gu.Ptr(""),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"url not parsable, error",
"Endpoint not parsable, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
URL: gu.Ptr("://"),
Endpoint: gu.Ptr("://"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@ -394,7 +374,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
},
@ -406,7 +386,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
Name: gu.Ptr("name"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsNotFound,
@ -415,18 +395,10 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"no changes",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("target", "instance"),
),
),
),
@ -439,35 +411,27 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
TargetType: gu.Ptr(domain.TargetTypeWebhook),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("target", "instance"),
),
),
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
[]target.Changes{
target.ChangeName("name", "name2"),
},
@ -483,7 +447,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
Name: gu.Ptr("name2"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsPreconditionFailed,
@ -492,23 +456,15 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
expectPush(
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
[]target.Changes{
target.ChangeName("name", "name2"),
},
@ -524,40 +480,31 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
Name: gu.Ptr("name2"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
{
"push full ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
expectPush(
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
[]target.Changes{
target.ChangeName("name", "name2"),
target.ChangeURL("https://example2.com"),
target.ChangeTargetType(domain.TargetTypeRequestResponse),
target.ChangeTimeout(time.Second),
target.ChangeAsync(true),
target.ChangeEndpoint("https://example2.com"),
target.ChangeTargetType(domain.TargetTypeCall),
target.ChangeTimeout(10 * time.Second),
target.ChangeInterruptOnError(true),
},
),
@ -571,17 +518,16 @@ func TestCommands_ChangeTarget(t *testing.T) {
AggregateID: "id1",
},
Name: gu.Ptr("name2"),
URL: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeRequestResponse),
Timeout: gu.Ptr(time.Second),
Async: gu.Ptr(true),
Endpoint: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
@ -589,7 +535,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
if tt.res.err == nil {
@ -607,7 +553,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
func TestCommands_DeleteTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@ -627,12 +573,12 @@ func TestCommands_DeleteTarget(t *testing.T) {
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
id: "",
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@ -641,14 +587,14 @@ func TestCommands_DeleteTarget(t *testing.T) {
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsNotFound,
@ -657,23 +603,15 @@ func TestCommands_DeleteTarget(t *testing.T) {
{
"remove ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
expectPush(
target.NewRemovedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
"name",
),
),
@ -682,11 +620,11 @@ func TestCommands_DeleteTarget(t *testing.T) {
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
@ -694,7 +632,7 @@ func TestCommands_DeleteTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
if tt.res.err == nil {

View File

@ -31,3 +31,17 @@ func (e ExecutionType) String() string {
}
return ""
}
type ExecutionTargetType uint
func (s ExecutionTargetType) Valid() bool {
return s < executionTargetTypeStateCount
}
const (
ExecutionTargetTypeUnspecified ExecutionTargetType = iota
ExecutionTargetTypeInclude
ExecutionTargetTypeTarget
executionTargetTypeStateCount
)

View File

@ -4,7 +4,8 @@ type TargetType uint
const (
TargetTypeWebhook TargetType = iota
TargetTypeRequestResponse
TargetTypeCall
TargetTypeAsync
)
type TargetState int32

View File

@ -0,0 +1,122 @@
package execution
import (
"bytes"
"context"
"io"
"net/http"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ContextInfo interface {
GetHTTPRequestBody() []byte
GetContent() interface{}
SetHTTPResponseBody([]byte) error
}
type Target interface {
GetTargetID() string
IsInterruptOnError() bool
GetEndpoint() string
GetTargetType() domain.TargetType
GetTimeout() time.Duration
}
// CallTargets call a list of targets in order with handling of error and responses
func CallTargets(
ctx context.Context,
targets []Target,
info ContextInfo,
) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
for _, target := range targets {
// call the type of target
resp, err := CallTarget(ctx, target, info)
// handle error if interrupt is set
if err != nil && target.IsInterruptOnError() {
return nil, err
}
if len(resp) > 0 {
// error in unmarshalling
if err := info.SetHTTPResponseBody(resp); err != nil {
return nil, err
}
}
}
return info.GetContent(), nil
}
type ContextInfoRequest interface {
GetHTTPRequestBody() []byte
}
// CallTarget call the desired type of target with handling of responses
func CallTarget(
ctx context.Context,
target Target,
info ContextInfoRequest,
) (res []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
switch target.GetTargetType() {
// get request, ignore response and return request and error for handling in list of targets
case domain.TargetTypeWebhook:
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())
case domain.TargetTypeAsync:
go func(target Target, info ContextInfoRequest) {
if _, err := call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
}
}(target, info)
return nil, nil
default:
return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown")
}
}
// 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)
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) {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, span := tracing.NewSpan(ctx)
defer func() {
cancel()
span.EndWithError(err)
}()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Check for success between 200 and 299, redirect 300 to 399 is handled by the client, return error with statusCode >= 400
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
return io.ReadAll(resp.Body)
}
return nil, zerrors.ThrowUnknown(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")
}

View File

@ -0,0 +1,347 @@
package execution
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
)
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
timeout time.Duration
sleep time.Duration
method string
body []byte
respBody []byte
statusCode int
}
type res struct {
body []byte
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"not ok status",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusBadRequest,
},
res{
wantErr: true,
},
},
{
"timeout",
args{
ctx: context.Background(),
timeout: time.Second,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
},
res{
wantErr: true,
},
},
{
"ok",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
},
res{
body: []byte("{\"response\": \"values\"}"),
},
},
}
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,
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
)
if tt.res.wantErr {
assert.Error(t, err)
assert.Nil(t, respBody)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.res.body, respBody)
}
})
}
}
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
target *mockTarget
sleep time.Duration
info ContextInfoRequest
method string
body []byte
respBody []byte
statusCode int
}
type res struct {
body []byte
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"unknown targettype, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: 4,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
},
},
{
"webhook, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
},
},
{
"webhook, ok",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
res{
body: nil,
},
},
{
"request response, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
},
},
{
"request response, ok",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
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\"}"),
},
},
}
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),
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.body, respBody)
})
}
}

View File

@ -65,7 +65,9 @@ func AssertListDetails[D ListDetailsMsg](t testing.TB, expected, actual D) {
assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult())
if wantDetails.GetTimestamp() != nil {
gotCD := gotDetails.GetTimestamp().AsTime()
wantCD := time.Now()
assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute))
}
}

View File

@ -522,39 +522,32 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj
require.NoError(t, err)
}
func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *action.CreateTargetResponse {
func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse {
nameSet := fmt.Sprint(time.Now().UnixNano() + 1)
if name != "" {
nameSet = name
}
req := &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Name: nameSet,
Endpoint: endpoint,
Timeout: durationpb.New(10 * time.Second),
}
target, err := s.Client.ActionV3.CreateTarget(ctx, req)
require.NoError(t, err)
return target
}
func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T, name string, async bool, interrupt bool) *action.CreateTargetResponse {
req := &action.CreateTargetRequest{
Name: name,
TargetType: &action.CreateTargetRequest_RestWebhook{
switch ty {
case domain.TargetTypeWebhook:
req.TargetType = &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: interrupt,
},
}
case domain.TargetTypeCall:
req.TargetType = &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: interrupt,
},
Timeout: durationpb.New(10 * time.Second),
}
if async {
req.ExecutionType = &action.CreateTargetRequest_IsAsync{
IsAsync: true,
}
}
if interrupt {
req.ExecutionType = &action.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
case domain.TargetTypeAsync:
req.TargetType = &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
}
}
target, err := s.Client.ActionV3.CreateTarget(ctx, req)
@ -562,16 +555,22 @@ func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T,
return target
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string, includes []string) *action.SetExecutionResponse {
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,
Includes: includes,
})
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{
Condition: cond,
})
require.NoError(t, err)
}
func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse {
return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1))
}

View File

@ -3,7 +3,10 @@ package query
import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"errors"
"time"
sq "github.com/Masterminds/squirrel"
@ -11,6 +14,8 @@ import (
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query/projection"
exec "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -23,18 +28,10 @@ var (
name: projection.ExecutionIDCol,
table: executionTable,
}
ExecutionColumnCreationDate = Column{
name: projection.ExecutionCreationDateCol,
table: executionTable,
}
ExecutionColumnChangeDate = Column{
name: projection.ExecutionChangeDateCol,
table: executionTable,
}
ExecutionColumnResourceOwner = Column{
name: projection.ExecutionResourceOwnerCol,
table: executionTable,
}
ExecutionColumnInstanceID = Column{
name: projection.ExecutionInstanceIDCol,
table: executionTable,
@ -43,14 +40,33 @@ var (
name: projection.ExecutionSequenceCol,
table: executionTable,
}
ExecutionColumnTargets = Column{
name: projection.ExecutionTargetsCol,
table: executionTable,
executionTargetsTable = table{
name: projection.ExecutionTable + "_" + projection.ExecutionTargetSuffix,
instanceIDCol: projection.ExecutionTargetInstanceIDCol,
}
ExecutionColumnIncludes = Column{
name: projection.ExecutionIncludesCol,
table: executionTable,
executionTargetsTableAlias = executionTargetsTable.setAlias("execution_targets")
ExecutionTargetsColumnInstanceID = Column{
name: projection.ExecutionTargetInstanceIDCol,
table: executionTargetsTableAlias,
}
ExecutionTargetsColumnExecutionID = Column{
name: projection.ExecutionTargetExecutionIDCol,
table: executionTargetsTableAlias,
}
executionTargetsListCol = Column{
name: "targets",
table: executionTargetsTableAlias,
}
)
var (
//go:embed execution_targets.sql
executionTargetsQuery string
//go:embed targets_by_execution_id.sql
TargetsByExecutionIDQuery string
//go:embed targets_by_execution_ids.sql
TargetsByExecutionIDsQuery string
)
type Executions struct {
@ -66,8 +82,7 @@ type Execution struct {
ID string
domain.ObjectDetails
Targets database.TextArray[string]
Includes database.TextArray[string]
Targets []*exec.Target
}
type ExecutionSearchQueries struct {
@ -108,39 +123,211 @@ func NewExecutionTypeSearchQuery(t domain.ExecutionType) (SearchQuery, error) {
return NewTextQuery(ExecutionColumnID, t.String(), TextStartsWith)
}
func NewExecutionTargetSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(ExecutionColumnTargets, value, TextListContains)
func NewTargetSearchQuery(target string) (SearchQuery, error) {
data, err := targetItemJSONB(domain.ExecutionTargetTypeTarget, target)
if err != nil {
return nil, err
}
return NewListContains(executionTargetsListCol, data)
}
func NewExecutionIncludeSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(ExecutionColumnIncludes, value, TextListContains)
func NewIncludeSearchQuery(include string) (SearchQuery, error) {
data, err := targetItemJSONB(domain.ExecutionTargetTypeInclude, include)
if err != nil {
return nil, err
}
return NewListContains(executionTargetsListCol, data)
}
// marshall executionTargets into the same JSONB structure as in the SQL queries
func targetItemJSONB(t domain.ExecutionTargetType, targetItem string) ([]byte, error) {
var target *executionTarget
switch t {
case domain.ExecutionTargetTypeTarget:
target = &executionTarget{Target: targetItem}
case domain.ExecutionTargetTypeInclude:
target = &executionTarget{Include: targetItem}
case domain.ExecutionTargetTypeUnspecified:
return nil, nil
default:
return nil, nil
}
return json.Marshal([]*executionTarget{target})
}
// TargetsByExecutionID query list of targets for best match of a list of IDs, for example:
// [ "request/zitadel.action.v3alpha.ActionService/GetTargetByID",
// "request/zitadel.action.v3alpha.ActionService",
// "request" ]
func (q *Queries) TargetsByExecutionID(ctx context.Context, ids []string) (execution []*ExecutionTarget, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
instanceID := authz.GetInstance(ctx).InstanceID()
if instanceID == "" {
return nil, nil
}
err = q.client.QueryContext(ctx,
func(rows *sql.Rows) error {
execution, err = scanExecutionTargets(rows)
return err
},
TargetsByExecutionIDQuery,
instanceID,
database.TextArray[string](ids),
)
return execution, err
}
// TargetsByExecutionIDs query list of targets for best matches of 2 separate lists of IDs, combined for performance, for example:
// [ "request/zitadel.action.v3alpha.ActionService/GetTargetByID",
// "request/zitadel.action.v3alpha.ActionService",
// "request" ]
// and
// [ "response/zitadel.action.v3alpha.ActionService/GetTargetByID",
// "response/zitadel.action.v3alpha.ActionService",
// "response" ]
func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*ExecutionTarget, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
instanceID := authz.GetInstance(ctx).InstanceID()
if instanceID == "" {
return nil, nil
}
err = q.client.QueryContext(ctx,
func(rows *sql.Rows) error {
execution, err = scanExecutionTargets(rows)
return err
},
TargetsByExecutionIDsQuery,
instanceID,
database.TextArray[string](ids1),
database.TextArray[string](ids2),
)
return execution, err
}
func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) {
return sq.Select(
ExecutionColumnInstanceID.identifier(),
ExecutionColumnID.identifier(),
ExecutionColumnChangeDate.identifier(),
ExecutionColumnSequence.identifier(),
executionTargetsListCol.identifier(),
).From(executionTable.identifier()).
Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " +
ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " +
ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(),
).
PlaceholderFormat(sq.Dollar),
scanExecution
}
func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) {
return sq.Select(
ExecutionColumnInstanceID.identifier(),
ExecutionColumnID.identifier(),
ExecutionColumnChangeDate.identifier(),
ExecutionColumnResourceOwner.identifier(),
ExecutionColumnSequence.identifier(),
ExecutionColumnTargets.identifier(),
ExecutionColumnIncludes.identifier(),
executionTargetsListCol.identifier(),
countColumn.identifier(),
).From(executionTable.identifier()).
Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " +
ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " +
ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(),
).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Executions, error) {
executions := make([]*Execution, 0)
var count uint64
for rows.Next() {
scanExecutions
}
type executionTarget struct {
Position int `json:"position,omitempty"`
Include string `json:"include,omitempty"`
Target string `json:"target,omitempty"`
}
func scanExecution(row *sql.Row) (*Execution, error) {
execution := new(Execution)
err := rows.Scan(
targets := make([]byte, 0)
err := row.Scan(
&execution.ResourceOwner,
&execution.ID,
&execution.EventDate,
&execution.ResourceOwner,
&execution.Sequence,
&execution.Targets,
&execution.Includes,
&targets,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal")
}
executionTargets := make([]*executionTarget, 0)
if err := json.Unmarshal(targets, &executionTargets); err != nil {
return nil, err
}
execution.Targets = make([]*exec.Target, len(executionTargets))
for i := range executionTargets {
if executionTargets[i].Target != "" {
execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: executionTargets[i].Target}
}
if executionTargets[i].Include != "" {
execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: executionTargets[i].Include}
}
}
return execution, nil
}
func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) {
executionTargets := make([]*executionTarget, 0)
if err := json.Unmarshal(data, &executionTargets); err != nil {
return nil, err
}
targets := make([]*exec.Target, len(executionTargets))
// position starts with 1
for _, item := range executionTargets {
if item.Target != "" {
targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target}
}
if item.Include != "" {
targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include}
}
}
return targets, nil
}
func scanExecutions(rows *sql.Rows) (*Executions, error) {
executions := make([]*Execution, 0)
var count uint64
for rows.Next() {
execution := new(Execution)
targets := make([]byte, 0)
err := rows.Scan(
&execution.ResourceOwner,
&execution.ID,
&execution.EventDate,
&execution.Sequence,
&targets,
&count,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-tbrmno85vp", "Errors.Execution.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-tyw2ydsj84", "Errors.Internal")
}
execution.Targets, err = executionTargetsUnmarshal(targets)
if err != nil {
return nil, err
}
@ -148,7 +335,7 @@ func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-72xfx5jlj7", "Errors.Query.CloseRows")
return nil, zerrors.ThrowInternal(err, "QUERY-yhka3fs3mw", "Errors.Query.CloseRows")
}
return &Executions{
@ -157,35 +344,80 @@ func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
Count: count,
},
}, nil
}
}
func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) {
return sq.Select(
ExecutionColumnID.identifier(),
ExecutionColumnChangeDate.identifier(),
ExecutionColumnResourceOwner.identifier(),
ExecutionColumnSequence.identifier(),
ExecutionColumnTargets.identifier(),
ExecutionColumnIncludes.identifier(),
).From(executionTable.identifier()).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Execution, error) {
execution := new(Execution)
err := row.Scan(
&execution.ID,
&execution.EventDate,
&execution.ResourceOwner,
&execution.Sequence,
&execution.Targets,
&execution.Includes,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal")
}
return execution, nil
}
type ExecutionTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *ExecutionTarget) GetExecutionID() string {
return e.ExecutionID
}
func (e *ExecutionTarget) GetTargetID() string {
return e.TargetID
}
func (e *ExecutionTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *ExecutionTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *ExecutionTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *ExecutionTarget) GetTimeout() time.Duration {
return e.Timeout
}
func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
targets := make([]*ExecutionTarget, 0)
for rows.Next() {
target := new(ExecutionTarget)
var (
instanceID = &sql.NullString{}
executionID = &sql.NullString{}
targetID = &sql.NullString{}
targetType = &sql.NullInt32{}
endpoint = &sql.NullString{}
timeout = &sql.NullInt64{}
interruptOnError = &sql.NullBool{}
)
err := rows.Scan(
executionID,
instanceID,
targetID,
targetType,
endpoint,
timeout,
interruptOnError,
)
if err != nil {
return nil, err
}
target.InstanceID = instanceID.String
target.ExecutionID = executionID.String
target.TargetID = targetID.String
target.TargetType = domain.TargetType(targetType.Int32)
target.Endpoint = endpoint.String
target.Timeout = time.Duration(timeout.Int64)
target.InterruptOnError = interruptOnError.Bool
targets = append(targets, target)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-37ardr0pki", "Errors.Query.CloseRows")
}
return targets, nil
}

View File

@ -0,0 +1,11 @@
SELECT instance_id,
execution_id,
JSONB_AGG(
JSON_OBJECT(
'position' : position,
'include' : include,
'target' : target_id
)
) as targets
FROM projections.executions1_targets
GROUP BY instance_id, execution_id

View File

@ -8,44 +8,56 @@ import (
"regexp"
"testing"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
exec "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
prepareExecutionsStmt = `SELECT projections.executions.id,` +
` projections.executions.change_date,` +
` projections.executions.resource_owner,` +
` projections.executions.sequence,` +
` projections.executions.targets,` +
` projections.executions.includes,` +
prepareExecutionsStmt = `SELECT projections.executions1.instance_id,` +
` projections.executions1.id,` +
` projections.executions1.change_date,` +
` projections.executions1.sequence,` +
` execution_targets.targets,` +
` COUNT(*) OVER ()` +
` FROM projections.executions`
` FROM projections.executions1` +
` JOIN (` +
`SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` +
` FROM projections.executions1_targets` +
` GROUP BY instance_id, execution_id` +
`)` +
` AS execution_targets` +
` ON execution_targets.instance_id = projections.executions1.instance_id` +
` AND execution_targets.execution_id = projections.executions1.id`
prepareExecutionsCols = []string{
"instance_id",
"id",
"change_date",
"resource_owner",
"sequence",
"targets",
"includes",
"count",
}
prepareExecutionStmt = `SELECT projections.executions.id,` +
` projections.executions.change_date,` +
` projections.executions.resource_owner,` +
` projections.executions.sequence,` +
` projections.executions.targets,` +
` projections.executions.includes` +
` FROM projections.executions`
prepareExecutionStmt = `SELECT projections.executions1.instance_id,` +
` projections.executions1.id,` +
` projections.executions1.change_date,` +
` projections.executions1.sequence,` +
` execution_targets.targets` +
` FROM projections.executions1` +
` JOIN (` +
`SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` +
` FROM projections.executions1_targets` +
` GROUP BY instance_id, execution_id` +
`)` +
` AS execution_targets` +
` ON execution_targets.instance_id = projections.executions1.instance_id` +
` AND execution_targets.execution_id = projections.executions1.id`
prepareExecutionCols = []string{
"instance_id",
"id",
"change_date",
"resource_owner",
"sequence",
"targets",
"includes",
}
)
@ -81,12 +93,11 @@ func Test_ExecutionPrepares(t *testing.T) {
prepareExecutionsCols,
[][]driver.Value{
{
"ro",
"id",
testNow,
"ro",
uint64(20211109),
database.TextArray[string]{"target"},
database.TextArray[string]{"include"},
[]byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`),
},
},
),
@ -103,8 +114,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211109,
},
Targets: database.TextArray[string]{"target"},
Includes: database.TextArray[string]{"include"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
},
},
},
@ -118,20 +131,18 @@ func Test_ExecutionPrepares(t *testing.T) {
prepareExecutionsCols,
[][]driver.Value{
{
"ro",
"id-1",
testNow,
"ro",
uint64(20211109),
database.TextArray[string]{"target1"},
database.TextArray[string]{"include1"},
[]byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`),
},
{
"ro",
"id-2",
testNow,
"ro",
uint64(20211110),
database.TextArray[string]{"target2"},
database.TextArray[string]{"include2"},
[]byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`),
},
},
),
@ -148,8 +159,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211109,
},
Targets: database.TextArray[string]{"target1"},
Includes: database.TextArray[string]{"include1"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
},
{
ID: "id-2",
@ -158,8 +171,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211110,
},
Targets: database.TextArray[string]{"target2"},
Includes: database.TextArray[string]{"include2"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
},
},
},
@ -207,12 +222,11 @@ func Test_ExecutionPrepares(t *testing.T) {
regexp.QuoteMeta(prepareExecutionStmt),
prepareExecutionCols,
[]driver.Value{
"ro",
"id",
testNow,
"ro",
uint64(20211109),
database.TextArray[string]{"target"},
database.TextArray[string]{"include"},
[]byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`),
},
),
},
@ -223,8 +237,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211109,
},
Targets: database.TextArray[string]{"target"},
Includes: database.TextArray[string]{"include"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
},
},
{

View File

@ -3,6 +3,7 @@ package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
@ -11,15 +12,19 @@ import (
)
const (
ExecutionTable = "projections.executions"
ExecutionTable = "projections.executions1"
ExecutionIDCol = "id"
ExecutionCreationDateCol = "creation_date"
ExecutionChangeDateCol = "change_date"
ExecutionResourceOwnerCol = "resource_owner"
ExecutionInstanceIDCol = "instance_id"
ExecutionSequenceCol = "sequence"
ExecutionTargetsCol = "targets"
ExecutionIncludesCol = "includes"
ExecutionTargetSuffix = "targets"
ExecutionTargetExecutionIDCol = "execution_id"
ExecutionTargetInstanceIDCol = "instance_id"
ExecutionTargetPositionCol = "position"
ExecutionTargetTargetIDCol = "target_id"
ExecutionTargetIncludeCol = "include"
)
type executionProjection struct{}
@ -33,19 +38,28 @@ func (*executionProjection) Name() string {
}
func (*executionProjection) Init() *old_handler.Check {
return handler.NewTableCheck(
return handler.NewMultiTableCheck(
handler.NewTable([]*handler.InitColumn{
handler.NewColumn(ExecutionIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionCreationDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(ExecutionChangeDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(ExecutionResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(ExecutionTargetsCol, handler.ColumnTypeTextArray, handler.Nullable()),
handler.NewColumn(ExecutionIncludesCol, handler.ColumnTypeTextArray, handler.Nullable()),
handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(ExecutionInstanceIDCol, ExecutionIDCol),
),
handler.NewSuffixedTable([]*handler.InitColumn{
handler.NewColumn(ExecutionTargetInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionTargetExecutionIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionTargetPositionCol, handler.ColumnTypeInt64),
handler.NewColumn(ExecutionTargetIncludeCol, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(ExecutionTargetTargetIDCol, handler.ColumnTypeText, handler.Nullable()),
},
handler.NewPrimaryKey(ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol, ExecutionTargetPositionCol),
ExecutionTargetSuffix,
handler.WithForeignKey(handler.NewForeignKey("execution", []string{ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol}, []string{ExecutionInstanceIDCol, ExecutionIDCol})),
handler.WithIndex(handler.NewIndex("execution", []string{ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol})),
),
)
}
@ -55,7 +69,7 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer {
Aggregate: exec.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: exec.SetEventType,
Event: exec.SetEventV2Type,
Reduce: p.reduceExecutionSet,
},
{
@ -77,21 +91,65 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer {
}
func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*exec.SetEvent](event)
e, err := assertEvent[*exec.SetEventV2](event)
if err != nil {
return nil, err
}
columns := []handler.Column{
stmts := []func(eventstore.Event) handler.Exec{
handler.AddUpsertStatement(
[]handler.Column{
handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionIDCol, e.Aggregate().ID),
},
[]handler.Column{
handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionIDCol, e.Aggregate().ID),
handler.NewCol(ExecutionResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(ExecutionCreationDateCol, handler.OnlySetValueOnInsert(ExecutionTable, e.CreationDate())),
handler.NewCol(ExecutionChangeDateCol, e.CreationDate()),
handler.NewCol(ExecutionSequenceCol, e.Sequence()),
handler.NewCol(ExecutionTargetsCol, e.Targets),
handler.NewCol(ExecutionIncludesCol, e.Includes),
},
),
// cleanup execution targets to re-insert them
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(ExecutionTargetExecutionIDCol, e.Aggregate().ID),
},
handler.WithTableSuffix(ExecutionTargetSuffix),
),
}
return handler.NewUpsertStatement(e, columns[0:2], columns), nil
if len(e.Targets) > 0 {
for i, target := range e.Targets {
var targetStr, includeStr string
switch target.Type {
case domain.ExecutionTargetTypeTarget:
targetStr = target.Target
case domain.ExecutionTargetTypeInclude:
includeStr = target.Target
case domain.ExecutionTargetTypeUnspecified:
continue
default:
continue
}
stmts = append(stmts,
handler.AddCreateStatement(
[]handler.Column{
handler.NewCol(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionTargetExecutionIDCol, e.Aggregate().ID),
handler.NewCol(ExecutionTargetPositionCol, i+1),
handler.NewCol(ExecutionTargetIncludeCol, includeStr),
handler.NewCol(ExecutionTargetTargetIDCol, targetStr),
},
handler.WithTableSuffix(ExecutionTargetSuffix),
),
)
}
}
return handler.NewMultiStatement(e, stmts...), nil
}
func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) {
@ -99,8 +157,8 @@ func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*h
if err != nil {
return nil, err
}
return handler.NewDeleteStatement(
e,
return handler.NewDeleteStatement(e,
[]handler.Condition{
handler.NewCond(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(ExecutionIDCol, e.Aggregate().ID),

View File

@ -25,11 +25,11 @@ func TestExecutionProjection_reduces(t *testing.T) {
args: args{
event: getEvent(
testEvent(
exec.SetEventType,
exec.SetEventV2Type,
exec.AggregateType,
[]byte(`{"targets": ["target"], "includes": ["include"]}`),
[]byte(`{"targets": [{"type":2,"target":"target"},{"type":1,"target":"include"}]}`),
),
eventstore.GenericEventMapper[exec.SetEvent],
eventstore.GenericEventMapper[exec.SetEventV2],
),
},
reduce: (&executionProjection{}).reduceExecutionSet,
@ -39,16 +39,40 @@ func TestExecutionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.executions (instance_id, id, resource_owner, creation_date, change_date, sequence, targets, includes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, id) DO UPDATE SET (resource_owner, creation_date, change_date, sequence, targets, includes) = (EXCLUDED.resource_owner, projections.executions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.targets, EXCLUDED.includes)",
expectedStmt: "INSERT INTO projections.executions1 (instance_id, id, creation_date, change_date, sequence) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (instance_id, id) DO UPDATE SET (creation_date, change_date, sequence) = (projections.executions1.creation_date, EXCLUDED.change_date, EXCLUDED.sequence)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
"ro-id",
anyArg{},
anyArg{},
uint64(15),
[]string{"target"},
[]string{"include"},
},
},
{
expectedStmt: "DELETE FROM projections.executions1_targets WHERE (instance_id = $1) AND (execution_id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
},
},
{
expectedStmt: "INSERT INTO projections.executions1_targets (instance_id, execution_id, position, include, target_id) VALUES ($1, $2, $3, $4, $5)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
1,
"",
"target",
},
},
{
expectedStmt: "INSERT INTO projections.executions1_targets (instance_id, execution_id, position, include, target_id) VALUES ($1, $2, $3, $4, $5)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
2,
"include",
"",
},
},
},
@ -74,7 +98,7 @@ func TestExecutionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1) AND (id = $2)",
expectedStmt: "DELETE FROM projections.executions1 WHERE (instance_id = $1) AND (id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
@ -103,7 +127,7 @@ func TestExecutionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.executions1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@ -269,8 +269,8 @@ func newProjectionsList() {
RestrictionsProjection,
SystemFeatureProjection,
InstanceFeatureProjection,
ExecutionProjection,
TargetProjection,
ExecutionProjection,
UserSchemaProjection,
}
}

View File

@ -11,7 +11,7 @@ import (
)
const (
TargetTable = "projections.targets"
TargetTable = "projections.targets1"
TargetIDCol = "id"
TargetCreationDateCol = "creation_date"
TargetChangeDateCol = "change_date"
@ -20,9 +20,8 @@ const (
TargetSequenceCol = "sequence"
TargetNameCol = "name"
TargetTargetType = "target_type"
TargetURLCol = "url"
TargetEndpointCol = "endpoint"
TargetTimeoutCol = "timeout"
TargetAsyncCol = "async"
TargetInterruptOnErrorCol = "interrupt_on_error"
)
@ -47,10 +46,9 @@ func (*targetProjection) Init() *old_handler.Check {
handler.NewColumn(TargetTargetType, handler.ColumnTypeEnum),
handler.NewColumn(TargetSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(TargetNameCol, handler.ColumnTypeText),
handler.NewColumn(TargetURLCol, handler.ColumnTypeText, handler.Default("")),
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64, handler.Default(0)),
handler.NewColumn(TargetAsyncCol, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText),
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
},
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
),
@ -103,10 +101,9 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S
handler.NewCol(TargetChangeDateCol, e.CreationDate()),
handler.NewCol(TargetSequenceCol, e.Sequence()),
handler.NewCol(TargetNameCol, e.Name),
handler.NewCol(TargetURLCol, e.URL),
handler.NewCol(TargetEndpointCol, e.Endpoint),
handler.NewCol(TargetTargetType, e.TargetType),
handler.NewCol(TargetTimeoutCol, e.Timeout),
handler.NewCol(TargetAsyncCol, e.Async),
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
},
), nil
@ -128,15 +125,12 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler
if e.TargetType != nil {
values = append(values, handler.NewCol(TargetTargetType, *e.TargetType))
}
if e.URL != nil {
values = append(values, handler.NewCol(TargetURLCol, *e.URL))
if e.Endpoint != nil {
values = append(values, handler.NewCol(TargetEndpointCol, *e.Endpoint))
}
if e.Timeout != nil {
values = append(values, handler.NewCol(TargetTimeoutCol, *e.Timeout))
}
if e.Async != nil {
values = append(values, handler.NewCol(TargetAsyncCol, *e.Async))
}
if e.InterruptOnError != nil {
values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError))
}

View File

@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) {
testEvent(
target.AddedEventType,
target.AggregateType,
[]byte(`{"name": "name", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
),
eventstore.GenericEventMapper[target.AddedEvent],
),
@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.targets (instance_id, resource_owner, id, creation_date, change_date, sequence, name, url, target_type, timeout, async, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
expectedStmt: "INSERT INTO projections.targets1 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
expectedArgs: []interface{}{
"instance-id",
"ro-id",
@ -54,7 +54,6 @@ func TestTargetProjection_reduces(t *testing.T) {
domain.TargetTypeWebhook,
3 * time.Second,
true,
true,
},
},
},
@ -68,7 +67,7 @@ func TestTargetProjection_reduces(t *testing.T) {
testEvent(
target.ChangedEventType,
target.AggregateType,
[]byte(`{"name": "name2", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
),
eventstore.GenericEventMapper[target.ChangedEvent],
),
@ -80,7 +79,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.targets SET (change_date, sequence, resource_owner, name, target_type, url, timeout, async, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)",
expectedStmt: "UPDATE projections.targets1 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (instance_id = $9) AND (id = $10)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@ -90,7 +89,6 @@ func TestTargetProjection_reduces(t *testing.T) {
"https://example.com",
3 * time.Second,
true,
true,
"instance-id",
"agg-id",
},
@ -118,7 +116,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1) AND (id = $2)",
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1) AND (id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
@ -147,7 +145,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@ -478,31 +478,31 @@ func (q *SubSelect) comp() sq.Sqlizer {
return selectQuery
}
type ListQuery struct {
type listQuery struct {
Column Column
Data interface{}
Compare ListComparison
}
func NewListQuery(column Column, value interface{}, compare ListComparison) (*ListQuery, error) {
func NewListQuery(column Column, value interface{}, compare ListComparison) (*listQuery, error) {
if compare < 0 || compare >= listCompareMax {
return nil, ErrInvalidCompare
}
if column.isZero() {
return nil, ErrMissingColumn
}
return &ListQuery{
return &listQuery{
Column: column,
Data: value,
Compare: compare,
}, nil
}
func (q *ListQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
func (q *listQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query.Where(q.comp())
}
func (q *ListQuery) comp() sq.Sqlizer {
func (q *listQuery) comp() sq.Sqlizer {
if q.Compare != ListIn {
return nil
}
@ -517,7 +517,7 @@ func (q *ListQuery) comp() sq.Sqlizer {
return sq.Eq{q.Column.identifier(): q.Data}
}
func (q *ListQuery) Col() Column {
func (q *listQuery) Col() Column {
return q.Column
}
@ -720,6 +720,25 @@ type listContains struct {
args interface{}
}
func NewListContains(c Column, value interface{}) (*listContains, error) {
return &listContains{
col: c,
args: value,
}, nil
}
func (q *listContains) Col() Column {
return q.col
}
func (q *listContains) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query.Where(q.comp())
}
func (q *listContains) ToSql() (string, []interface{}, error) {
return q.col.identifier() + " @> ? ", []interface{}{q.args}, nil
}
func (q *listContains) comp() sq.Sqlizer {
return q
}

View File

@ -521,7 +521,7 @@ func TestNewListQuery(t *testing.T) {
tests := []struct {
name string
args args
want *ListQuery
want *listQuery
wantErr func(error) bool
}{
{
@ -575,7 +575,7 @@ func TestNewListQuery(t *testing.T) {
data: []interface{}{"hurst"},
compare: ListIn,
},
want: &ListQuery{
want: &listQuery{
Column: testCol,
Data: []interface{}{"hurst"},
Compare: ListIn,
@ -588,7 +588,7 @@ func TestNewListQuery(t *testing.T) {
data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}},
compare: ListIn,
},
want: &ListQuery{
want: &listQuery{
Column: testCol,
Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}},
Compare: ListIn,
@ -751,7 +751,7 @@ func TestListQuery_comp(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &ListQuery{
s := &listQuery{
Column: tt.fields.Column,
Data: tt.fields.Data,
Compare: tt.fields.Compare,

View File

@ -52,17 +52,13 @@ var (
table: targetTable,
}
TargetColumnURL = Column{
name: projection.TargetURLCol,
name: projection.TargetEndpointCol,
table: targetTable,
}
TargetColumnTimeout = Column{
name: projection.TargetTimeoutCol,
table: targetTable,
}
TargetColumnAsync = Column{
name: projection.TargetAsyncCol,
table: targetTable,
}
TargetColumnInterruptOnError = Column{
name: projection.TargetInterruptOnErrorCol,
table: targetTable,
@ -84,9 +80,8 @@ type Target struct {
Name string
TargetType domain.TargetType
URL string
Endpoint string
Timeout time.Duration
Async bool
InterruptOnError bool
}
@ -138,7 +133,6 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
TargetColumnTargetType.identifier(),
TargetColumnTimeout.identifier(),
TargetColumnURL.identifier(),
TargetColumnAsync.identifier(),
TargetColumnInterruptOnError.identifier(),
countColumn.identifier(),
).From(targetTable.identifier()).
@ -156,8 +150,7 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&target.Name,
&target.TargetType,
&target.Timeout,
&target.URL,
&target.Async,
&target.Endpoint,
&target.InterruptOnError,
&count,
)
@ -190,7 +183,6 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild
TargetColumnTargetType.identifier(),
TargetColumnTimeout.identifier(),
TargetColumnURL.identifier(),
TargetColumnAsync.identifier(),
TargetColumnInterruptOnError.identifier(),
).From(targetTable.identifier()).
PlaceholderFormat(sq.Dollar),
@ -204,8 +196,7 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild
&target.Name,
&target.TargetType,
&target.Timeout,
&target.URL,
&target.Async,
&target.Endpoint,
&target.InterruptOnError,
)
if err != nil {

View File

@ -14,18 +14,17 @@ import (
)
var (
prepareTargetsStmt = `SELECT projections.targets.id,` +
` projections.targets.change_date,` +
` projections.targets.resource_owner,` +
` projections.targets.sequence,` +
` projections.targets.name,` +
` projections.targets.target_type,` +
` projections.targets.timeout,` +
` projections.targets.url,` +
` projections.targets.async,` +
` projections.targets.interrupt_on_error,` +
prepareTargetsStmt = `SELECT projections.targets1.id,` +
` projections.targets1.change_date,` +
` projections.targets1.resource_owner,` +
` projections.targets1.sequence,` +
` projections.targets1.name,` +
` projections.targets1.target_type,` +
` projections.targets1.timeout,` +
` projections.targets1.endpoint,` +
` projections.targets1.interrupt_on_error,` +
` COUNT(*) OVER ()` +
` FROM projections.targets`
` FROM projections.targets1`
prepareTargetsCols = []string{
"id",
"change_date",
@ -34,23 +33,21 @@ var (
"name",
"target_type",
"timeout",
"url",
"async",
"endpoint",
"interrupt_on_error",
"count",
}
prepareTargetStmt = `SELECT projections.targets.id,` +
` projections.targets.change_date,` +
` projections.targets.resource_owner,` +
` projections.targets.sequence,` +
` projections.targets.name,` +
` projections.targets.target_type,` +
` projections.targets.timeout,` +
` projections.targets.url,` +
` projections.targets.async,` +
` projections.targets.interrupt_on_error` +
` FROM projections.targets`
prepareTargetStmt = `SELECT projections.targets1.id,` +
` projections.targets1.change_date,` +
` projections.targets1.resource_owner,` +
` projections.targets1.sequence,` +
` projections.targets1.name,` +
` projections.targets1.target_type,` +
` projections.targets1.timeout,` +
` projections.targets1.endpoint,` +
` projections.targets1.interrupt_on_error` +
` FROM projections.targets1`
prepareTargetCols = []string{
"id",
"change_date",
@ -59,8 +56,7 @@ var (
"name",
"target_type",
"timeout",
"url",
"async",
"endpoint",
"interrupt_on_error",
}
)
@ -106,7 +102,6 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
true,
},
},
),
@ -126,8 +121,7 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: true,
Endpoint: "https://example.com",
InterruptOnError: true,
},
},
@ -151,7 +145,6 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
false,
},
{
"id-2",
@ -163,14 +156,24 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
false,
true,
},
{
"id-3",
testNow,
"ro",
uint64(20211110),
"target-name3",
domain.TargetTypeAsync,
1 * time.Second,
"https://example.com",
false,
},
},
),
},
object: &Targets{
SearchResponse: SearchResponse{
Count: 2,
Count: 3,
},
Targets: []*Target{
{
@ -183,9 +186,8 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name1",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: true,
InterruptOnError: false,
Endpoint: "https://example.com",
InterruptOnError: true,
},
{
ID: "id-2",
@ -197,9 +199,21 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name2",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: false,
InterruptOnError: true,
Endpoint: "https://example.com",
InterruptOnError: false,
},
{
ID: "id-3",
ObjectDetails: domain.ObjectDetails{
EventDate: testNow,
ResourceOwner: "ro",
Sequence: 20211110,
},
Name: "target-name3",
TargetType: domain.TargetTypeAsync,
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: false,
},
},
},
@ -256,7 +270,6 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
false,
},
),
},
@ -270,9 +283,8 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: true,
InterruptOnError: false,
Endpoint: "https://example.com",
InterruptOnError: true,
},
},
{

View File

@ -0,0 +1,40 @@
WITH RECURSIVE
dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
AS (SELECT execution_id
, instance_id
, ARRAY [position]
, "include"
, "target_id"
FROM matched_targets_and_includes
UNION ALL
SELECT e.execution_id
, p.instance_id
, e.position || p.position
, p."include"
, p."target_id"
FROM dissolved_execution_targets e
JOIN projections.executions1_targets p
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
AND e.include = p.execution_id),
matched AS (SELECT *
FROM projections.executions1
WHERE instance_id = $1
AND id = ANY($2)
ORDER BY id DESC
LIMIT 1),
matched_targets_and_includes AS (SELECT pos.*
FROM matched m
JOIN
projections.executions1_targets pos
ON m.id = pos.execution_id
AND m.instance_id = pos.instance_id
ORDER BY execution_id,
position)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
FROM dissolved_execution_targets e
JOIN projections.targets1 t
ON e.instance_id = t.instance_id
AND e.target_id = t.id
WHERE "include" = ''
ORDER BY position DESC;

View File

@ -0,0 +1,47 @@
WITH RECURSIVE
dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
AS (SELECT execution_id
, instance_id
, ARRAY [position]
, "include"
, "target_id"
FROM matched_targets_and_includes
UNION ALL
SELECT e.execution_id
, p.instance_id
, e.position || p.position
, p."include"
, p."target_id"
FROM dissolved_execution_targets e
JOIN projections.executions1_targets p
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
AND e.include = p.execution_id),
matched AS ((SELECT *
FROM projections.executions1
WHERE instance_id = $1
AND id = ANY($2)
ORDER BY id DESC
LIMIT 1)
UNION ALL
(SELECT *
FROM projections.executions1
WHERE instance_id = $1
AND id = ANY($3)
ORDER BY id DESC
LIMIT 1)),
matched_targets_and_includes AS (SELECT pos.*
FROM matched m
JOIN
projections.executions1_targets pos
ON m.id = pos.execution_id
AND m.instance_id = pos.instance_id
ORDER BY execution_id,
position)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
FROM dissolved_execution_targets e
JOIN projections.targets1 t
ON e.instance_id = t.instance_id
AND e.target_id = t.id
WHERE "include" = ''
ORDER BY position DESC;

View File

@ -23,7 +23,10 @@ func NewAggregate(aggrID, instanceID string) *eventstore.Aggregate {
}
func ID(executionType domain.ExecutionType, value string) string {
return strings.Join([]string{executionType.String(), value}, ".")
if strings.HasPrefix(value, "/") {
return strings.Join([]string{executionType.String(), value}, "")
}
return strings.Join([]string{executionType.String(), value}, "/")
}
func IDAll(executionType domain.ExecutionType) string {

View File

@ -4,5 +4,6 @@ import "github.com/zitadel/zitadel/internal/eventstore"
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SetEventType, eventstore.GenericEventMapper[SetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, SetEventV2Type, eventstore.GenericEventMapper[SetEventV2])
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent])
}

View File

@ -3,12 +3,14 @@ package execution
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
eventTypePrefix eventstore.EventType = "execution."
SetEventType = eventTypePrefix + "set"
SetEventV2Type = eventTypePrefix + "v2.set"
RemovedEventType = eventTypePrefix + "removed"
)
@ -31,18 +33,39 @@ func (e *SetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewSetEvent(
type SetEventV2 struct {
*eventstore.BaseEvent `json:"-"`
Targets []*Target `json:"targets"`
}
func (e *SetEventV2) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *SetEventV2) Payload() any {
return e
}
func (e *SetEventV2) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
type Target struct {
Type domain.ExecutionTargetType `json:"type"`
Target string `json:"target"`
}
func NewSetEventV2(
ctx context.Context,
aggregate *eventstore.Aggregate,
targets []string,
includes []string,
) *SetEvent {
return &SetEvent{
targets []*Target,
) *SetEventV2 {
return &SetEventV2{
BaseEvent: eventstore.NewBaseEventForPush(
ctx, aggregate, SetEventType,
ctx, aggregate, SetEventV2Type,
),
Targets: targets,
Includes: includes,
}
}

View File

@ -20,9 +20,8 @@ type AddedEvent struct {
Name string `json:"name"`
TargetType domain.TargetType `json:"targetType"`
URL string `json:"url"`
Endpoint string `json:"endpoint"`
Timeout time.Duration `json:"timeout"`
Async bool `json:"async"`
InterruptOnError bool `json:"interruptOnError"`
}
@ -43,16 +42,15 @@ func NewAddedEvent(
aggregate *eventstore.Aggregate,
name string,
targetType domain.TargetType,
url string,
endpoint string,
timeout time.Duration,
async bool,
interruptOnError bool,
) *AddedEvent {
return &AddedEvent{
*eventstore.NewBaseEventForPush(
ctx, aggregate, AddedEventType,
),
name, targetType, url, timeout, async, interruptOnError}
name, targetType, endpoint, timeout, interruptOnError}
}
type ChangedEvent struct {
@ -60,9 +58,8 @@ type ChangedEvent struct {
Name *string `json:"name,omitempty"`
TargetType *domain.TargetType `json:"targetType,omitempty"`
URL *string `json:"url,omitempty"`
Endpoint *string `json:"endpoint,omitempty"`
Timeout *time.Duration `json:"timeout,omitempty"`
Async *bool `json:"async,omitempty"`
InterruptOnError *bool `json:"interruptOnError,omitempty"`
oldName string
@ -119,9 +116,9 @@ func ChangeTargetType(targetType domain.TargetType) func(event *ChangedEvent) {
}
}
func ChangeURL(url string) func(event *ChangedEvent) {
func ChangeEndpoint(endpoint string) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.URL = &url
e.Endpoint = &endpoint
}
}
@ -131,12 +128,6 @@ func ChangeTimeout(timeout time.Duration) func(event *ChangedEvent) {
}
}
func ChangeAsync(async bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Async = &async
}
}
func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.InterruptOnError = &interruptOnError

View File

@ -484,6 +484,7 @@ Errors:
NotActive: Действието не е активно
NotInactive: Действието не е неактивно
MaxAllowed: Не са разрешени допълнителни активни действия
NotEnabled: Функцията „Действие“ не е активирана
Flow:
FlowTypeMissing: Липсва FlowType
Empty: Потокът вече е празен

View File

@ -470,6 +470,7 @@ Errors:
NotActive: Akce není aktivní
NotInactive: Akce není neaktivní
MaxAllowed: Není dovoleno více aktivních akcí
NotEnabled: Funkce "Akce" není povolena
Flow:
FlowTypeMissing: Chybí typ toku
Empty: Tok je již prázdný

View File

@ -470,6 +470,7 @@ Errors:
NotActive: Action ist nicht aktiv
NotInactive: Action ist nicht inaktiv
MaxAllowed: Keine weitere aktiven Actions mehr erlaubt
NotEnabled: Function "Action" ist nicht aktiviert
Flow:
FlowTypeMissing: FlowType fehlt
Empty: Flow ist bereits leer

View File

@ -470,6 +470,7 @@ Errors:
NotActive: Action is not active
NotInactive: Action is not inactive
MaxAllowed: No additional active Actions allowed
NotEnabled: Feature "Action" is not enabled
Flow:
FlowTypeMissing: FlowType missing
Empty: Flow is already empty

View File

@ -470,6 +470,7 @@ Errors:
NotActive: La acción no está activa
NotInactive: La acción no está inactiva
MaxAllowed: No hay acciones adicionales activas permitidas
NotEnabled: La función "Acción" no está habilitada
Flow:
FlowTypeMissing: Falta el tipo de flujo
Empty: El flujo ya está vacío

View File

@ -470,6 +470,7 @@ Errors:
NotActive: L'action n'est pas active
NotInactive: L'action n'est pas inactive
MaxAllowed: Aucune action active supplémentaire n'est autorisée
NotEnabled: La fonctionnalité "Action" n'est pas activée
Flow:
FlowTypeMissing: FlowType missing
Empty: Le flux est déjà vide

View File

@ -470,6 +470,7 @@ Errors:
NotActive: L'azione non è attiva
NotInactive: L'azione non è inattiva
MaxAllowed: Non sono permesse altre azioni attive
NotEnabled: La funzione "Azione" non è abilitata
Flow:
FlowTypeMissing: FlowType mancante
Empty: Flow è già vuoto

View File

@ -459,6 +459,7 @@ Errors:
NotActive: アクションはアクティブではありません
NotInactive: アクションは非アクティブではありません
MaxAllowed: 追加のアクティブアクションは許可されていません
NotEnabled: 機能「アクション」が有効になっていません
Flow:
FlowTypeMissing: フロータイプがありません
Empty: フローはすでに空です

View File

@ -469,6 +469,7 @@ Errors:
NotActive: Акцијата не е активна
NotInactive: Акцијата не е неактивна
MaxAllowed: Не се дозволени дополнителни активни акции
NotEnabled: Функцијата „Акција“ не е овозможена
Flow:
FlowTypeMissing: FlowType не е наведен
Empty: Flow е веќе празен

View File

@ -469,6 +469,7 @@ Errors:
NotActive: Actie is niet actief
NotInactive: Actie is niet inactief
MaxAllowed: Geen extra actieve acties toegestaan
NotEnabled: Functie "Actie" is niet ingeschakeld
Flow:
FlowTypeMissing: FlowType ontbreekt
Empty: Flow is al leeg

View File

@ -470,6 +470,7 @@ Errors:
NotActive: Działanie nie jest aktywne
NotInactive: Działanie nie jest dezaktywowane
MaxAllowed: Nie dopuszcza się dodatkowych aktywnych działań.
NotEnabled: Funkcja „Akcja” nie jest włączona
Flow:
FlowTypeMissing: Typ przepływu brakuje
Empty: Przepływ jest już pusty

View File

@ -469,6 +469,7 @@ Errors:
NotActive: A ação não está ativa
NotInactive: A ação não está inativa
MaxAllowed: Não são permitidas ações adicionais ativas
NotEnabled: O recurso "Ação" não está ativado
Flow:
FlowTypeMissing: O tipo de fluxo está faltando
Empty: O fluxo já está vazio

View File

@ -463,6 +463,7 @@ Errors:
NotActive: Действие не активно
NotInactive: Действие не является неактивным
MaxAllowed: Дополнительные активные действия запрещены
NotEnabled: Функция «Действие» не включена
Flow:
FlowTypeMissing: Тип процесса отсутствует
Empty: Процесс уже пуст

View File

@ -470,6 +470,7 @@ Errors:
NotActive: 动作不是启用状态
NotInactive: 动作不是停用状态
MaxAllowed: 不允许额外的动作
NotEnabled: 未启用“操作”功能
Flow:
FlowTypeMissing: 缺少身份认证流程类型
Empty: 身份认证流程为空

View File

@ -423,22 +423,26 @@ message CreateTargetRequest {
option (validate.required) = true;
SetRESTWebhook rest_webhook = 2;
SetRESTRequestResponse rest_request_response = 3;
SetRESTCall rest_call = 3;
SetRESTAsync rest_async = 4;
}
// Timeout defines the duration until ZITADEL cancels the execution.
google.protobuf.Duration timeout = 4 [
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\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 5;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 6;
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 {
@ -472,21 +476,24 @@ message UpdateTargetRequest {
// or its target URL.
oneof target_type {
SetRESTWebhook rest_webhook = 3;
SetRESTRequestResponse rest_request_response = 4;
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 = 5 [
optional google.protobuf.Duration timeout = 6 [
(validate.rules).duration = {gt: {seconds: 0}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 6;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 7;
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 {
@ -554,10 +561,8 @@ message GetTargetByIDResponse {
message SetExecutionRequest {
// Defines the condition type and content of the condition for execution.
Condition condition = 1;
// Defines the execution targets which are defined as a different resource, which are called in the defined conditions.
repeated string targets = 2;
// Defines other executions as included with the same condition-types.
repeated string includes = 3;
// Ordered list of targets/includes called during the execution.
repeated zitadel.action.v3alpha.ExecutionTargetType targets = 2;
}
message SetExecutionResponse {

View File

@ -14,17 +14,20 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action";
message Execution {
string execution_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"request.zitadel.session.v2beta.SessionService\"";
}
];
Condition Condition = 1;
// Details provide some base information (such as the last change date) of the target.
zitadel.object.v2beta.Details details = 2;
// Targets which are called in the defined conditions.
repeated string targets = 3;
// Included executions with the same condition-types.
repeated string includes = 4;
// List of ordered list of targets/includes called during the execution.
repeated ExecutionTargetType targets = 3;
}
message ExecutionTargetType {
oneof type {
// Unique identifier of existing target to call.
string target = 1;
// Unique identifier of existing execution to include targets of.
Condition include = 2;
}
}
message Condition {
@ -37,7 +40,7 @@ message Condition {
// Condition-type to execute on response if a request on the defined API point happens.
ResponseExecution response = 2;
// Condition-type to execute if function is used, replaces actions v1.
string function = 3;
FunctionExecution function = 3;
// Condition-type to execute if an event is created in the system.
EventExecution event = 4;
}
@ -95,6 +98,11 @@ message ResponseExecution {
}
}
// Executed on the specified function
message FunctionExecution {
string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1000}];
}
message EventExecution{
// Condition for the event execution, only one possible.
oneof condition{

View File

@ -43,7 +43,7 @@ message TargetQuery {
message IncludeQuery {
// Defines the include to query for.
string include = 1 [
Condition include = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the id of the include"
example: "\"request.zitadel.session.v2beta.SessionService\"";

View File

@ -13,30 +13,21 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action";
// Wait for response but response body is ignored, status is checked, call is sent as post.
message SetRESTWebhook {
string url = 1 [
(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\"";
}
];
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 1;
}
message SetRESTRequestResponse {
string url = 1 [
(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\"";
}
];
// Wait for response and response body is used, status is checked, call is sent as post.
message SetRESTCall {
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 1;
}
// 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 [
@ -56,18 +47,19 @@ message Target {
// Defines the target type and how the response of the target is treated.
oneof target_type {
SetRESTWebhook rest_webhook = 4;
SetRESTRequestResponse rest_request_response = 5;
SetRESTCall rest_call = 5;
SetRESTAsync rest_async = 6;
}
// Timeout defines the duration until ZITADEL cancels the execution.
google.protobuf.Duration timeout = 6 [
google.protobuf.Duration timeout = 7 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 7;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 8;
string endpoint = 8 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://example.com/hooks/ip_check\"";
}
];
}