feat: actions v2 api GA (#10364)

# Which Problems Are Solved

The Actions v2beta API is not yet promoted to GA.

# How the Problems Are Solved

Promote Actions v2Beta API to Actions v2 API.

# Additional Changes

None

# Additional Context

None
This commit is contained in:
Stefan Benz
2025-07-31 15:09:09 +02:00
parent 01c02d7807
commit 096e92f5ec
21 changed files with 5314 additions and 65 deletions

View File

@@ -34,6 +34,7 @@ import (
"github.com/zitadel/zitadel/internal/api"
"github.com/zitadel/zitadel/internal/api/assets"
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
action_v2 "github.com/zitadel/zitadel/internal/api/grpc/action/v2"
action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/admin"
app "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta"
@@ -509,6 +510,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, action_v2.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}

View File

@@ -0,0 +1,92 @@
package action
import (
"context"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
)
func (s *Server) SetExecution(ctx context.Context, req *connect.Request[action.SetExecutionRequest]) (*connect.Response[action.SetExecutionResponse], error) {
reqTargets := req.Msg.GetTargets()
targets := make([]*execution.Target, len(reqTargets))
for i, target := range reqTargets {
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target}
}
set := &command.SetExecution{
Targets: targets,
}
var err error
var details *domain.ObjectDetails
instanceID := authz.GetInstance(ctx).InstanceID()
switch t := req.Msg.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID)
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, instanceID)
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, instanceID)
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, instanceID)
default:
err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid")
}
if err != nil {
return nil, err
}
return connect.NewResponse(&action.SetExecutionResponse{
SetDate: timestamppb.New(details.EventDate),
}), nil
}
func (s *Server) ListExecutionFunctions(ctx context.Context, _ *connect.Request[action.ListExecutionFunctionsRequest]) (*connect.Response[action.ListExecutionFunctionsResponse], error) {
return connect.NewResponse(&action.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}), nil
}
func (s *Server) ListExecutionMethods(ctx context.Context, _ *connect.Request[action.ListExecutionMethodsRequest]) (*connect.Response[action.ListExecutionMethodsResponse], error) {
return connect.NewResponse(&action.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}), nil
}
func (s *Server) ListExecutionServices(ctx context.Context, _ *connect.Request[action.ListExecutionServicesRequest]) (*connect.Response[action.ListExecutionServicesResponse], error) {
return connect.NewResponse(&action.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}), 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(),
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,784 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
)
func TestServer_GetTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error
req *action.GetTargetRequest
}
tests := []struct {
name string
args args
want *action.GetTargetResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.GetTargetRequest{},
},
wantErr: true,
},
{
name: "not found",
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.GetTargetRequest{Id: "notexisting"},
},
wantErr: true,
},
{
name: "get, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, async, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, webhook interruptOnError, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, call, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
{
name: "get, call interruptOnError, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true)
request.Id = resp.GetId()
response.Target.Id = resp.GetId()
response.Target.Name = name
response.Target.CreationDate = resp.GetCreationDate()
response.Target.ChangeDate = resp.GetCreationDate()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, err := instance.Client.ActionV2.GetTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, err, "Error: "+err.Error())
return
}
assert.NoError(ttt, err)
assert.EqualExportedValues(ttt, tt.want, got)
}, retryDuration, tick, "timeout waiting for expected target Executions")
})
}
}
func TestServer_ListTargets(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse)
req *action.ListTargetsRequest
}
tests := []struct {
name string
args args
want *action.ListTargetsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.ListTargetsRequest{},
},
wantErr: true,
},
{
name: "list, not found",
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{
{Filter: &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{"notfound"},
},
},
},
},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 0,
AppliedLimit: 100,
},
Targets: []*action.Target{},
},
},
{
name: "list single id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{resp.GetId()},
},
}
response.Targets[0].Id = resp.GetId()
response.Targets[0].Name = name
response.Targets[0].CreationDate = resp.GetCreationDate()
response.Targets[0].ChangeDate = resp.GetCreationDate()
response.Targets[0].SigningKey = resp.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
}, {
name: "list single name",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
name := gofakeit.Name()
resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{
TargetNameFilter: &action.TargetNameFilter{
TargetName: name,
},
}
response.Targets[0].Id = resp.GetId()
response.Targets[0].Name = name
response.Targets[0].CreationDate = resp.GetCreationDate()
response.Targets[0].ChangeDate = resp.GetCreationDate()
response.Targets[0].SigningKey = resp.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
},
{
name: "list multiple id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) {
name1 := gofakeit.Name()
name2 := gofakeit.Name()
name3 := gofakeit.Name()
resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false)
resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true)
resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false)
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
},
}
response.Targets[2].Id = resp1.GetId()
response.Targets[2].Name = name1
response.Targets[2].CreationDate = resp1.GetCreationDate()
response.Targets[2].ChangeDate = resp1.GetCreationDate()
response.Targets[2].SigningKey = resp1.GetSigningKey()
response.Targets[1].Id = resp2.GetId()
response.Targets[1].Name = name2
response.Targets[1].CreationDate = resp2.GetCreationDate()
response.Targets[1].ChangeDate = resp2.GetCreationDate()
response.Targets[1].SigningKey = resp2.GetSigningKey()
response.Targets[0].Id = resp3.GetId()
response.Targets[0].Name = name3
response.Targets[0].CreationDate = resp3.GetCreationDate()
response.Targets[0].ChangeDate = resp3.GetCreationDate()
response.Targets[0].SigningKey = resp3.GetSigningKey()
},
req: &action.ListTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.ListTargetsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 3,
AppliedLimit: 100,
},
Targets: []*action.Target{
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(5 * time.Second),
},
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(5 * time.Second),
},
{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(5 * time.Second),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := instance.Client.ActionV2.ListTargets(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr, "Error: "+listErr.Error())
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Targets, len(tt.want.Targets)) {
for i := range tt.want.Targets {
assert.EqualExportedValues(ttt, tt.want.Targets[i], got.Targets[i])
}
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution Executions")
})
}
}
func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) {
assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit)
assert.Equal(t, expected.TotalResult, actual.TotalResult)
}
func TestServer_ListExecutions(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse)
req *action.ListExecutionsRequest
}
tests := []struct {
name string
args args
want *action.ListExecutionsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.ListExecutionsRequest{},
},
wantErr: true,
},
{
name: "list request single condition",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
// Set expected response with used values for SetExecution
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
response.Executions[0].Condition = cond
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
}},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
},
},
},
{
name: "list request single target",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
// add target as Filter to the request
request.Filters[0] = &action.ExecutionSearchFilter{
Filter: &action.ExecutionSearchFilter_TargetFilter{
TargetFilter: &action.TargetFilter{
TargetId: target.GetId(),
},
},
}
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/UpdateAction",
},
},
},
}
resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()})
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
response.Executions[0].Condition = cond
response.Executions[0].Targets = []string{target.GetId()}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 1,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{
Condition: &action.Condition{},
Targets: []string{""},
},
},
},
},
{
name: "list multiple conditions",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
request.Filters[0] = &action.ExecutionSearchFilter{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
}},
{ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/CreateSession",
},
},
}},
{ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/SetSession",
},
},
}},
},
},
},
}
cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()})
response.Executions[2] = &action.Execution{
CreationDate: resp1.GetSetDate(),
ChangeDate: resp1.GetSetDate(),
Condition: cond1,
Targets: []string{targetResp.GetId()},
}
cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1]
resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()})
response.Executions[1] = &action.Execution{
CreationDate: resp2.GetSetDate(),
ChangeDate: resp2.GetSetDate(),
Condition: cond2,
Targets: []string{targetResp.GetId()},
}
cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2]
resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()})
response.Executions[0] = &action.Execution{
CreationDate: resp3.GetSetDate(),
ChangeDate: resp3.GetSetDate(),
Condition: cond3,
Targets: []string{targetResp.GetId()},
}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{
{},
},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 3,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{}, {}, {},
},
},
},
{
name: "list multiple conditions all types",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
response.Executions[(len(conditions)-1)-i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
Condition: cond,
Targets: []string{targetResp.GetId()},
}
}
},
req: &action.ListExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 10,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
},
},
},
{
name: "list multiple conditions all types, sort id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
response.Executions[i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
Condition: cond,
Targets: []string{targetResp.GetId()},
}
}
},
req: &action.ListExecutionsRequest{
SortingColumn: gu.Ptr(action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID),
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}},
{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}}}},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Pagination: &filter.PaginationResponse{
TotalResult: 10,
AppliedLimit: 100,
},
Executions: []*action.Execution{
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := instance.Client.ActionV2.ListExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(ttt, listErr, "Error: "+listErr.Error())
return
}
require.NoError(ttt, listErr)
// always first check length, otherwise its failed anyway
if assert.Len(ttt, got.Executions, len(tt.want.Executions)) {
assert.EqualExportedValues(ttt, got.Executions, tt.want.Executions)
}
assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination)
}, retryDuration, tick, "timeout waiting for expected execution Executions")
})
}
}

View File

@@ -0,0 +1,23 @@
//go:build integration
package action_test
import (
"context"
"os"
"testing"
"time"
)
var (
CTX context.Context
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
CTX = ctx
return m.Run()
}())
}

View File

@@ -0,0 +1,549 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
)
func TestServer_CreateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type want struct {
id bool
creationDate bool
signingKey bool
}
alreadyExistingTargetName := gofakeit.AppName()
instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", domain.TargetTypeAsync, false)
tests := []struct {
name string
ctx context.Context
req *action.CreateTargetRequest
want
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
},
wantErr: true,
},
{
name: "empty name",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, already existing, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: alreadyExistingTargetName,
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
wantErr: true,
},
{
name: "async, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "webhook, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "webhook, interrupt on error, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "call, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
{
name: "call, interruptOnError, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: want{
id: true,
creationDate: true,
signingKey: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
got, err := instance.Client.ActionV2.CreateTarget(tt.ctx, tt.req)
changeDate := time.Now().UTC()
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertCreateTargetResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, tt.want.signingKey, got)
})
}
}
func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID, expectedSigningKey bool, actualResp *action.CreateTargetResponse) {
if expectedCreationDate {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.CreationDate)
}
if expectedID {
assert.NotEmpty(t, actualResp.GetId())
} else {
assert.Nil(t, actualResp.Id)
}
if expectedSigningKey {
assert.NotEmpty(t, actualResp.GetSigningKey())
} else {
assert.Nil(t, actualResp.SigningKey)
}
}
func TestServer_UpdateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *action.UpdateTargetRequest
}
type want struct {
change bool
changeDate bool
signingKey bool
}
tests := []struct {
name string
prepare func(request *action.UpdateTargetRequest)
args args
want want
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.UpdateTargetRequest) {
request.Id = "notexisting"
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
wantErr: true,
},
{
name: "no change, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com"),
},
},
want: want{
change: false,
changeDate: true,
signingKey: false,
},
},
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "regenerate signingkey, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
ExpirationSigningKey: durationpb.New(0 * time.Second),
},
},
want: want{
change: true,
changeDate: true,
signingKey: true,
},
},
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
Timeout: durationpb.New(20 * time.Second),
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
{
name: "change type async, ok",
prepare: func(request *action.UpdateTargetRequest) {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId()
request.Id = targetID
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
},
},
},
want: want{
change: true,
changeDate: true,
signingKey: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
creationDate := time.Now().UTC()
tt.prepare(tt.args.req)
got, err := instance.Client.ActionV2.UpdateTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
return
}
changeDate := time.Time{}
if tt.want.change {
changeDate = time.Now().UTC()
}
assert.NoError(t, err)
assertUpdateTargetResponse(t, creationDate, changeDate, tt.want.changeDate, tt.want.signingKey, got)
})
}
}
func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate, expectedSigningKey bool, actualResp *action.UpdateTargetResponse) {
if expectedChangeDate {
if !changeDate.IsZero() {
assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate)
} else {
assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.ChangeDate)
}
if expectedSigningKey {
assert.NotEmpty(t, actualResp.GetSigningKey())
} else {
assert.Nil(t, actualResp.SigningKey)
}
}
func TestServer_DeleteTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
prepare func(request *action.DeleteTargetRequest) (time.Time, time.Time)
req *action.DeleteTargetRequest
wantDeletionDate bool
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.DeleteTargetRequest{
Id: "notexisting",
},
wantErr: true,
},
{
name: "empty id",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target, not existing",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "notexisting",
},
wantDeletionDate: false,
},
{
name: "delete target",
ctx: iamOwnerCtx,
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
return creationDate, time.Time{}
},
req: &action.DeleteTargetRequest{},
wantDeletionDate: true,
},
{
name: "delete target, already removed",
ctx: iamOwnerCtx,
prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) {
creationDate := time.Now().UTC()
targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.Id = targetID
instance.DeleteTarget(iamOwnerCtx, t, targetID)
return creationDate, time.Now().UTC()
},
req: &action.DeleteTargetRequest{},
wantDeletionDate: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var creationDate, deletionDate time.Time
if tt.prepare != nil {
creationDate, deletionDate = tt.prepare(tt.req)
}
got, err := instance.Client.ActionV2.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assertDeleteTargetResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got)
})
}
}
func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *action.DeleteTargetResponse) {
if expectedDeletionDate {
if !deletionDate.IsZero() {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate)
} else {
assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC())
}
} else {
assert.Nil(t, actualResp.DeletionDate)
}
}

View File

@@ -0,0 +1,404 @@
package action
import (
"context"
"strings"
"connectrpc.com/connect"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
)
const (
conditionIDAllSegmentCount = 0
conditionIDRequestResponseServiceSegmentCount = 1
conditionIDRequestResponseMethodSegmentCount = 2
conditionIDEventGroupSegmentCount = 1
)
func (s *Server) GetTarget(ctx context.Context, req *connect.Request[action.GetTargetRequest]) (*connect.Response[action.GetTargetResponse], error) {
resp, err := s.query.GetTargetByID(ctx, req.Msg.GetId())
if err != nil {
return nil, err
}
return connect.NewResponse(&action.GetTargetResponse{
Target: targetToPb(resp),
}), nil
}
type InstanceContext interface {
GetInstanceId() string
GetInstanceDomain() string
}
type Context interface {
GetOwner() InstanceContext
}
func (s *Server) ListTargets(ctx context.Context, req *connect.Request[action.ListTargetsRequest]) (*connect.Response[action.ListTargetsResponse], error) {
queries, err := s.ListTargetsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchTargets(ctx, queries)
if err != nil {
return nil, err
}
return connect.NewResponse(&action.ListTargetsResponse{
Targets: targetsToPb(resp.Targets),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func (s *Server) ListExecutions(ctx context.Context, req *connect.Request[action.ListExecutionsRequest]) (*connect.Response[action.ListExecutionsResponse], error) {
queries, err := s.ListExecutionsRequestToModel(req.Msg)
if err != nil {
return nil, err
}
resp, err := s.query.SearchExecutions(ctx, queries)
if err != nil {
return nil, err
}
return connect.NewResponse(&action.ListExecutionsResponse{
Executions: executionsToPb(resp.Executions),
Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse),
}), nil
}
func targetsToPb(targets []*query.Target) []*action.Target {
t := make([]*action.Target, len(targets))
for i, target := range targets {
t[i] = targetToPb(target)
}
return t
}
func targetToPb(t *query.Target) *action.Target {
target := &action.Target{
Id: t.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
SigningKey: t.SigningKey,
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.RESTWebhook{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeCall:
target.TargetType = &action.Target_RestCall{RestCall: &action.RESTCall{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeAsync:
target.TargetType = &action.Target_RestAsync{RestAsync: &action.RESTAsync{}}
default:
target.TargetType = nil
}
if !t.EventDate.IsZero() {
target.ChangeDate = timestamppb.New(t.EventDate)
}
if !t.CreationDate.IsZero() {
target.CreationDate = timestamppb.New(t.CreationDate)
}
return target
}
func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := targetQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.TargetSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func targetQueriesToQuery(queries []*action.TargetSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = targetQueryToQuery(qry)
if err != nil {
return nil, err
}
}
return q, nil
}
func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *action.TargetSearchFilter_TargetNameFilter:
return targetNameQueryToQuery(q.TargetNameFilter)
case *action.TargetSearchFilter_InTargetIdsFilter:
return targetInTargetIdsQueryToQuery(q.InTargetIdsFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) {
return query.NewTargetNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetTargetName())
}
func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) {
return query.NewTargetInIDsSearchQuery(q.GetTargetIds())
}
// targetFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column {
if field == nil {
return query.TargetColumnCreationDate
}
switch *field {
case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED:
return query.TargetColumnCreationDate
case action.TargetFieldName_TARGET_FIELD_NAME_ID:
return query.TargetColumnID
case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE:
return query.TargetColumnCreationDate
case action.TargetFieldName_TARGET_FIELD_NAME_CHANGED_DATE:
return query.TargetColumnChangeDate
case action.TargetFieldName_TARGET_FIELD_NAME_NAME:
return query.TargetColumnName
case action.TargetFieldName_TARGET_FIELD_NAME_TARGET_TYPE:
return query.TargetColumnTargetType
case action.TargetFieldName_TARGET_FIELD_NAME_URL:
return query.TargetColumnURL
case action.TargetFieldName_TARGET_FIELD_NAME_TIMEOUT:
return query.TargetColumnTimeout
case action.TargetFieldName_TARGET_FIELD_NAME_INTERRUPT_ON_ERROR:
return query.TargetColumnInterruptOnError
default:
return query.TargetColumnCreationDate
}
}
// executionFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.Column {
if field == nil {
return query.ExecutionColumnCreationDate
}
switch *field {
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED:
return query.ExecutionColumnCreationDate
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID:
return query.ExecutionColumnID
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE:
return query.ExecutionColumnCreationDate
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CHANGED_DATE:
return query.ExecutionColumnChangeDate
default:
return query.ExecutionColumnCreationDate
}
}
func (s *Server) ListExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination)
if err != nil {
return nil, err
}
queries, err := executionQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.ExecutionSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: executionFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func executionQueriesToQuery(queries []*action.ExecutionSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = executionQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.SearchQuery, error) {
switch q := searchQuery.Filter.(type) {
case *action.ExecutionSearchFilter_InConditionsFilter:
return inConditionsQueryToQuery(q.InConditionsFilter)
case *action.ExecutionSearchFilter_ExecutionTypeFilter:
return executionTypeToQuery(q.ExecutionTypeFilter)
case *action.ExecutionSearchFilter_TargetFilter:
return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionTypeToQuery(q *action.ExecutionTypeFilter) (query.SearchQuery, error) {
switch q.ExecutionType {
case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
case action.ExecutionType_EXECUTION_TYPE_REQUEST:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest)
case action.ExecutionType_EXECUTION_TYPE_RESPONSE:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse)
case action.ExecutionType_EXECUTION_TYPE_EVENT:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent)
case action.ExecutionType_EXECUTION_TYPE_FUNCTION:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction)
default:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
}
}
func inConditionsQueryToQuery(q *action.InConditionsFilter) (query.SearchQuery, error) {
values := make([]string, len(q.GetConditions()))
for i, condition := range q.GetConditions() {
id, err := conditionToID(condition)
if err != nil {
return nil, err
}
values[i] = id
}
return query.NewExecutionInIDsSearchQuery(values)
}
func conditionToID(q *action.Condition) (string, error) {
switch t := q.GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
return cond.ID(domain.ExecutionTypeResponse), nil
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
return cond.ID(), nil
case *action.Condition_Function:
return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionsToPb(executions []*query.Execution) []*action.Execution {
e := make([]*action.Execution, len(executions))
for i, execution := range executions {
e[i] = executionToPb(execution)
}
return e
}
func executionToPb(e *query.Execution) *action.Execution {
targets := make([]string, len(e.Targets))
for i := range e.Targets {
switch e.Targets[i].Type {
case domain.ExecutionTargetTypeTarget:
targets[i] = e.Targets[i].Target
case domain.ExecutionTargetTypeInclude, domain.ExecutionTargetTypeUnspecified:
continue
default:
continue
}
}
exec := &action.Execution{
Condition: executionIDToCondition(e.ID),
Targets: targets,
}
if !e.EventDate.IsZero() {
exec.ChangeDate = timestamppb.New(e.EventDate)
}
if !e.CreationDate.IsZero() {
exec.CreationDate = timestamppb.New(e.CreationDate)
}
return exec
}
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 conditionIDRequestResponseMethodSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}}
case conditionIDRequestResponseServiceSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case conditionIDAllSegmentCount:
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 conditionIDRequestResponseMethodSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}}
case conditionIDRequestResponseServiceSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case conditionIDAllSegmentCount:
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 conditionIDEventGroupSegmentCount:
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 conditionIDAllSegmentCount:
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

@@ -0,0 +1,71 @@
package action
import (
"net/http"
"connectrpc.com/connect"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
"github.com/zitadel/zitadel/pkg/grpc/action/v2/actionconnect"
)
var _ actionconnect.ActionServiceHandler = (*Server)(nil)
type Server struct {
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
ListGRPCMethods func() []string
ListGRPCServices func() []string
}
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
listActionFunctions func() []string,
listGRPCMethods func() []string,
listGRPCServices func() []string,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
ListActionFunctions: listActionFunctions,
ListGRPCMethods: listGRPCMethods,
ListGRPCServices: listGRPCServices,
}
}
func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) {
return actionconnect.NewActionServiceHandler(s, connect.WithInterceptors(interceptors...))
}
func (s *Server) FileDescriptor() protoreflect.FileDescriptor {
return action.File_zitadel_action_v2_action_service_proto
}
func (s *Server) AppName() string {
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ActionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterActionServiceHandler
}

View File

@@ -0,0 +1,123 @@
package action
import (
"context"
"connectrpc.com/connect"
"github.com/muhlemmer/gu"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
)
func (s *Server) CreateTarget(ctx context.Context, req *connect.Request[action.CreateTargetRequest]) (*connect.Response[action.CreateTargetResponse], error) {
add := createTargetToCommand(req.Msg)
instanceID := authz.GetInstance(ctx).InstanceID()
createdAt, err := s.command.AddTarget(ctx, add, instanceID)
if err != nil {
return nil, err
}
var creationDate *timestamppb.Timestamp
if !createdAt.IsZero() {
creationDate = timestamppb.New(createdAt)
}
return connect.NewResponse(&action.CreateTargetResponse{
Id: add.AggregateID,
CreationDate: creationDate,
SigningKey: add.SigningKey,
}), nil
}
func (s *Server) UpdateTarget(ctx context.Context, req *connect.Request[action.UpdateTargetRequest]) (*connect.Response[action.UpdateTargetResponse], error) {
instanceID := authz.GetInstance(ctx).InstanceID()
update := updateTargetToCommand(req.Msg)
changedAt, err := s.command.ChangeTarget(ctx, update, instanceID)
if err != nil {
return nil, err
}
var changeDate *timestamppb.Timestamp
if !changedAt.IsZero() {
changeDate = timestamppb.New(changedAt)
}
return connect.NewResponse(&action.UpdateTargetResponse{
ChangeDate: changeDate,
SigningKey: update.SigningKey,
}), nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *connect.Request[action.DeleteTargetRequest]) (*connect.Response[action.DeleteTargetResponse], error) {
instanceID := authz.GetInstance(ctx).InstanceID()
deletedAt, err := s.command.DeleteTarget(ctx, req.Msg.GetId(), instanceID)
if err != nil {
return nil, err
}
var deletionDate *timestamppb.Timestamp
if !deletedAt.IsZero() {
deletionDate = timestamppb.New(deletedAt)
}
return connect.NewResponse(&action.DeleteTargetResponse{
DeletionDate: deletionDate,
}), nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := req.GetTargetType().(type) {
case *action.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.CreateTargetRequest_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.CreateTargetRequest_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: req.GetName(),
TargetType: targetType,
Endpoint: req.GetEndpoint(),
Timeout: req.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget {
// TODO handle expiration, currently only immediate expiration is supported
expirationSigningKey := req.GetExpirationSigningKey() != nil
if req == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: req.Name,
Endpoint: req.Endpoint,
ExpirationSigningKey: expirationSigningKey,
}
if req.TargetType != nil {
switch t := req.GetTargetType().(type) {
case *action.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.UpdateTargetRequest_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.UpdateTargetRequest_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if req.Timeout != nil {
target.Timeout = gu.Ptr(req.GetTimeout().AsDuration())
}
return target
}

View File

@@ -0,0 +1,229 @@
package action
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.CreateTargetRequest
}
tests := []struct {
name string
args args
want *command.AddTarget
}{
{
name: "nil",
args: args{nil},
want: &command.AddTarget{
Name: "",
Endpoint: "",
Timeout: 0,
InterruptOnError: false,
},
},
{
name: "all fields (webhook)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
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.RESTAsync{},
},
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,
InterruptOnError: false,
},
},
{
name: "all fields (interrupting response)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeCall,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.UpdateTargetRequest
}
tests := []struct {
name string
args args
want *command.ChangeTarget
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields nil",
args: args{&action.UpdateTargetRequest{
Name: nil,
TargetType: nil,
Timeout: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
Endpoint: nil,
Timeout: nil,
InterruptOnError: nil,
},
},
{
name: "all fields empty",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
Endpoint: nil,
Timeout: gu.Ptr(0 * time.Second),
InterruptOnError: nil,
},
},
{
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.RESTWebhook{
InterruptOnError: false,
},
},
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(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.RESTWebhook{
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.RESTAsync{},
},
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),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (interrupting response)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -48,7 +48,7 @@ var (
func TestServer_ExecutionTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
fullMethod := action.ActionService_GetTarget_FullMethodName
tests := []struct {
@@ -164,8 +164,8 @@ func TestServer_ExecutionTarget(t *testing.T) {
}
},
clean: func(ctx context.Context) {
instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
deleteExecution(ctx, t, instance, conditionRequestFullMethod(fullMethod))
deleteExecution(ctx, t, instance, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetRequest{
Id: "something",
@@ -197,7 +197,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
}
},
clean: func(ctx context.Context) {
instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
deleteExecution(ctx, t, instance, conditionRequestFullMethod(fullMethod))
},
req: &action.GetTargetRequest{},
wantErr: true,
@@ -259,7 +259,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
}
},
clean: func(ctx context.Context) {
instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
deleteExecution(ctx, t, instance, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetRequest{},
wantErr: true,
@@ -290,9 +290,16 @@ func TestServer_ExecutionTarget(t *testing.T) {
}
}
func deleteExecution(ctx context.Context, t *testing.T, instance *integration.Instance, cond *action.Condition) {
_, err := instance.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
})
require.NoError(t, err)
}
func TestServer_ExecutionTarget_Event(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
event := "session.added"
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil)
@@ -349,7 +356,7 @@ func TestServer_ExecutionTarget_Event(t *testing.T) {
func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
event := "session.added"
// call takes longer than timeout of target
@@ -401,7 +408,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) {
func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
event := "session.added"
urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil)
@@ -463,7 +470,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T)
}
func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []string) {
instance.SetExecution(ctx, t, condition, targets)
setExecution(ctx, t, instance, condition, targets)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
@@ -488,11 +495,19 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in
}
}
}, retryDuration, tick, "timeout waiting for expected execution result")
return
}
func setExecution(ctx context.Context, t *testing.T, instance *integration.Instance, cond *action.Condition, targets []string) *action.SetExecutionResponse {
target, err := instance.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Targets: targets,
})
require.NoError(t, err)
return target
}
func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse {
resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt)
resp := createTarget(ctx, t, instance, "", endpoint, ty, interrupt)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
@@ -529,6 +544,38 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst
return resp
}
func createTarget(ctx context.Context, t *testing.T, instance *integration.Instance, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse {
if name == "" {
name = gofakeit.Name()
}
req := &action.CreateTargetRequest{
Name: name,
Endpoint: endpoint,
Timeout: durationpb.New(5 * time.Second),
}
switch ty {
case domain.TargetTypeWebhook:
req.TargetType = &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.RESTWebhook{
InterruptOnError: interrupt,
},
}
case domain.TargetTypeCall:
req.TargetType = &action.CreateTargetRequest_RestCall{
RestCall: &action.RESTCall{
InterruptOnError: interrupt,
},
}
case domain.TargetTypeAsync:
req.TargetType = &action.CreateTargetRequest_RestAsync{
RestAsync: &action.RESTAsync{},
}
}
target, err := instance.Client.ActionV2beta.CreateTarget(ctx, req)
require.NoError(t, err)
return target
}
func conditionRequestFullMethod(fullMethod string) *action.Condition {
return &action.Condition{
ConditionType: &action.Condition_Request{
@@ -577,8 +624,8 @@ func conditionFunction(function string) *action.Condition {
func TestServer_ExecutionTargetPreUserinfo(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin)
client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2)
require.NoError(t, err)
@@ -893,8 +940,8 @@ func contextInfoForUserOIDC(instance *integration.Instance, function string, cli
func TestServer_ExecutionTargetPreAccessToken(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin)
client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2)
require.NoError(t, err)
@@ -1086,8 +1133,8 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *
func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin)
isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin)
idpMetadata, err := instance.GetSAMLIDPMetadata()
require.NoError(t, err)

View File

@@ -17,7 +17,7 @@ import (
func TestServer_SetExecution_Request(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
@@ -29,7 +29,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -155,7 +155,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
deleteExecution(tt.ctx, t, instance, tt.req.GetCondition())
})
}
}
@@ -174,7 +174,7 @@ func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, e
func TestServer_SetExecution_Response(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
@@ -186,7 +186,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -311,14 +311,14 @@ func TestServer_SetExecution_Response(t *testing.T) {
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
deleteExecution(tt.ctx, t, instance, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Event(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
@@ -330,7 +330,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -474,14 +474,14 @@ func TestServer_SetExecution_Event(t *testing.T) {
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
deleteExecution(tt.ctx, t, instance, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Function(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
@@ -493,7 +493,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -559,7 +559,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
deleteExecution(tt.ctx, t, instance, tt.req.GetCondition())
})
}
}

View File

@@ -21,7 +21,7 @@ import (
func TestServer_GetTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error
@@ -36,7 +36,7 @@ func TestServer_GetTarget(t *testing.T) {
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.GetTargetRequest{},
},
wantErr: true,
@@ -213,7 +213,7 @@ func TestServer_GetTarget(t *testing.T) {
func TestServer_ListTargets(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse)
@@ -228,7 +228,7 @@ func TestServer_ListTargets(t *testing.T) {
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.ListTargetsRequest{},
},
wantErr: true,
@@ -445,7 +445,7 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes
func TestServer_ListExecutions(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
@@ -462,7 +462,7 @@ func TestServer_ListExecutions(t *testing.T) {
{
name: "missing permission",
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.ListExecutionsRequest{},
},
wantErr: true,
@@ -473,7 +473,7 @@ func TestServer_ListExecutions(t *testing.T) {
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
resp := setExecution(ctx, t, instance, cond, []string{targetResp.GetId()})
// Set expected response with used values for SetExecution
response.Executions[0].CreationDate = resp.GetSetDate()
@@ -542,7 +542,7 @@ func TestServer_ListExecutions(t *testing.T) {
},
},
}
resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()})
resp := setExecution(ctx, t, instance, cond, []string{target.GetId()})
response.Executions[0].CreationDate = resp.GetSetDate()
response.Executions[0].ChangeDate = resp.GetSetDate()
@@ -603,7 +603,7 @@ func TestServer_ListExecutions(t *testing.T) {
}
cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()})
resp1 := setExecution(ctx, t, instance, cond1, []string{targetResp.GetId()})
response.Executions[2] = &action.Execution{
CreationDate: resp1.GetSetDate(),
ChangeDate: resp1.GetSetDate(),
@@ -612,7 +612,7 @@ func TestServer_ListExecutions(t *testing.T) {
}
cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1]
resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()})
resp2 := setExecution(ctx, t, instance, cond2, []string{targetResp.GetId()})
response.Executions[1] = &action.Execution{
CreationDate: resp2.GetSetDate(),
ChangeDate: resp2.GetSetDate(),
@@ -621,7 +621,7 @@ func TestServer_ListExecutions(t *testing.T) {
}
cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2]
resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()})
resp3 := setExecution(ctx, t, instance, cond3, []string{targetResp.GetId()})
response.Executions[0] = &action.Execution{
CreationDate: resp3.GetSetDate(),
ChangeDate: resp3.GetSetDate(),
@@ -652,7 +652,7 @@ func TestServer_ListExecutions(t *testing.T) {
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
resp := setExecution(ctx, t, instance, cond, []string{targetResp.GetId()})
response.Executions[(len(conditions)-1)-i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),
@@ -708,7 +708,7 @@ func TestServer_ListExecutions(t *testing.T) {
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) {
conditions := request.Filters[0].GetInConditionsFilter().GetConditions()
for i, cond := range conditions {
resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()})
resp := setExecution(ctx, t, instance, cond, []string{targetResp.GetId()})
response.Executions[i] = &action.Execution{
CreationDate: resp.GetSetDate(),
ChangeDate: resp.GetSetDate(),

View File

@@ -19,7 +19,7 @@ import (
func TestServer_CreateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type want struct {
id bool
creationDate bool
@@ -36,7 +36,7 @@ func TestServer_CreateTarget(t *testing.T) {
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.CreateTargetRequest{
Name: gofakeit.Name(),
},
@@ -243,7 +243,7 @@ func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time
func TestServer_UpdateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *action.UpdateTargetRequest
@@ -267,7 +267,7 @@ func TestServer_UpdateTarget(t *testing.T) {
request.Id = targetID
},
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.UpdateTargetRequest{
Name: gu.Ptr(gofakeit.Name()),
},
@@ -278,7 +278,6 @@ func TestServer_UpdateTarget(t *testing.T) {
name: "not existing",
prepare: func(request *action.UpdateTargetRequest) {
request.Id = "notexisting"
return
},
args: args{
ctx: isolatedIAMOwnerCTX,
@@ -461,7 +460,7 @@ func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time
func TestServer_DeleteTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
@@ -472,7 +471,7 @@ func TestServer_DeleteTarget(t *testing.T) {
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
req: &action.DeleteTargetRequest{
Id: "notexisting",
},

View File

@@ -82,7 +82,7 @@ func targetsToPb(targets []*query.Target) []*action.Target {
func targetToPb(t *query.Target) *action.Target {
target := &action.Target{
Id: t.ObjectDetails.ID,
Id: t.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
@@ -99,11 +99,11 @@ func targetToPb(t *query.Target) *action.Target {
target.TargetType = nil
}
if !t.ObjectDetails.EventDate.IsZero() {
target.ChangeDate = timestamppb.New(t.ObjectDetails.EventDate)
if !t.EventDate.IsZero() {
target.ChangeDate = timestamppb.New(t.EventDate)
}
if !t.ObjectDetails.CreationDate.IsZero() {
target.CreationDate = timestamppb.New(t.ObjectDetails.CreationDate)
if !t.CreationDate.IsZero() {
target.CreationDate = timestamppb.New(t.CreationDate)
}
return target
}
@@ -334,11 +334,11 @@ func executionToPb(e *query.Execution) *action.Execution {
Condition: executionIDToCondition(e.ID),
Targets: targets,
}
if !e.ObjectDetails.EventDate.IsZero() {
exec.ChangeDate = timestamppb.New(e.ObjectDetails.EventDate)
if !e.EventDate.IsZero() {
exec.ChangeDate = timestamppb.New(e.EventDate)
}
if !e.ObjectDetails.CreationDate.IsZero() {
exec.CreationDate = timestamppb.New(e.ObjectDetails.CreationDate)
if !e.CreationDate.IsZero() {
exec.CreationDate = timestamppb.New(e.CreationDate)
}
return exec
}

View File

@@ -21,7 +21,8 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration/scim"
action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/action/v2"
action_v2beta "github.com/zitadel/zitadel/pkg/grpc/action/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/admin"
app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/auth"
@@ -69,7 +70,8 @@ type Client struct {
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2beta org_v2beta.OrganizationServiceClient
OrgV2 org.OrganizationServiceClient
ActionV2beta action.ActionServiceClient
ActionV2beta action_v2beta.ActionServiceClient
ActionV2 action.ActionServiceClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient
@@ -112,7 +114,8 @@ func newClient(ctx context.Context, target string) (*Client, error) {
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
OrgV2: org.NewOrganizationServiceClient(cc),
ActionV2beta: action.NewActionServiceClient(cc),
ActionV2beta: action_v2beta.NewActionServiceClient(cc),
ActionV2: action.NewActionServiceClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc),
@@ -1036,27 +1039,27 @@ func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoin
RestAsync: &action.RESTAsync{},
}
}
target, err := i.Client.ActionV2beta.CreateTarget(ctx, req)
target, err := i.Client.ActionV2.CreateTarget(ctx, req)
require.NoError(t, err)
return target
}
func (i *Instance) DeleteTarget(ctx context.Context, t *testing.T, id string) {
_, err := i.Client.ActionV2beta.DeleteTarget(ctx, &action.DeleteTargetRequest{
_, err := i.Client.ActionV2.DeleteTarget(ctx, &action.DeleteTargetRequest{
Id: id,
})
require.NoError(t, err)
}
func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
_, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{
_, err := i.Client.ActionV2.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
})
require.NoError(t, err)
}
func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string) *action.SetExecutionResponse {
target, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{
target, err := i.Client.ActionV2.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Targets: targets,
})

View File

@@ -0,0 +1,728 @@
syntax = "proto3";
package zitadel.action.v2;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/action/v2/target.proto";
import "zitadel/action/v2/execution.proto";
import "zitadel/action/v2/query.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/filter/v2/filter.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Action Service";
version: "2.0";
description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
consumes: "application/grpc";
produces: "application/json";
produces: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
host: "$CUSTOM-DOMAIN";
base_path: "/";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the resource.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
// Service to manage custom executions.
// The service provides methods to create, update, delete and list targets and executions.
service ActionService {
// Create Target
//
// Create a new target to your endpoint, which can be used in executions.
//
// Required permission:
// - `action.target.write`
//
// Required feature flag:
// - `actions`
rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) {
option (google.api.http) = {
post: "/v2/actions/targets"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target created successfully";
};
};
responses: {
key: "409"
value: {
description: "The target to create already exists.";
}
};
responses: {
key: "400"
value: {
description: "The feature flag `actions` is not enabled.";
}
};
};
}
// Update Target
//
// Update an existing target.
// To generate a new signing key set the optional expirationSigningKey.
//
// Required permission:
// - `action.target.write`
//
// Required feature flag:
// - `actions`
rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) {
option (google.api.http) = {
post: "/v2/actions/targets/{id}"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target successfully updated or left unchanged";
};
};
responses: {
key: "404"
value: {
description: "The target to update does not exist.";
}
};
responses: {
key: "400"
value: {
description: "The feature flag `actions` is not enabled.";
}
};
};
}
// Delete Target
//
// Delete an existing target. This will remove it from any configured execution as well.
// In case the target is not found, the request will return a successful response as
// the desired state is already achieved.
//
// Required permission:
// - `action.target.delete`
//
// Required feature flag:
// - `actions`
rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) {
option (google.api.http) = {
delete: "/v2/actions/targets/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target deleted successfully";
};
};
responses: {
key: "400"
value: {
description: "The feature flag `actions` is not enabled.";
}
};
};
}
// Get Target
//
// Returns the target identified by the requested ID.
//
// Required permission:
// - `action.target.read`
//
// Required feature flag:
// - `actions`
rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) {
option (google.api.http) = {
get: "/v2/actions/targets/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200"
value: {
description: "Target retrieved successfully";
}
};
responses: {
key: "404"
value: {
description: "The target to update does not exist.";
}
};
responses: {
key: "400"
value: {
description: "The feature flag `actions` is not enabled.";
}
};
};
}
// List targets
//
// List all matching targets. By default all targets of the instance are returned.
// Make sure to include a limit and sorting for pagination.
//
// Required permission:
// - `action.target.read`
//
// Required feature flag:
// - `actions`
rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) {
option (google.api.http) = {
post: "/v2/actions/targets/search",
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.target.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "A list of all targets matching the query";
};
};
responses: {
key: "400";
value: {
description: "invalid list query";
};
};
responses: {
key: "400"
value: {
description: "The feature flag `actions` is not enabled.";
}
};
};
}
// Set Execution
//
// Sets an execution to call a target or include the targets of another execution.
// Setting an empty list of targets will remove all targets from the execution, making it a noop.
//
// Required permission:
// - `action.execution.write`
//
// Required feature flag:
// - `actions`
rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) {
option (google.api.http) = {
put: "/v2/actions/executions"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.execution.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Execution successfully updated or left unchanged";
};
};
responses: {
key: "400"
value: {
description: "Condition to set execution does not exist or the feature flag `actions` is not enabled.";
}
};
};
}
// List Executions
//
// List all matching executions. By default all executions of the instance are returned that have at least one execution target.
// Make sure to include a limit and sorting for pagination.
//
// Required permission:
// - `action.execution.read`
//
// Required feature flag:
// - `actions`
rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) {
option (google.api.http) = {
post: "/v2/actions/executions/search"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "action.execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "A list of all non noop executions matching the query";
};
};
responses: {
key: "400";
value: {
description: "Invalid list query or the feature flag `actions` is not enabled.";
};
};
};
}
// List Execution Functions
//
// List all available functions which can be used as condition for executions.
rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) {
option (google.api.http) = {
get: "/v2/actions/executions/functions"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all functions successfully";
};
};
};
}
// List Execution Methods
//
// List all available methods which can be used as condition for executions.
rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) {
option (google.api.http) = {
get: "/v2/actions/executions/methods"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all methods successfully";
};
};
};
}
// List Execution Services
//
// List all available services which can be used as condition for executions.
rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) {
option (google.api.http) = {
get: "/v2/actions/executions/services"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all services successfully";
};
};
};
}
}
message CreateTargetRequest {
string name = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ip_allow_list\"";
min_length: 1
max_length: 1000
}
];
// Defines the target type and how the response of the target is treated.
oneof target_type {
option (validate.required) = true;
// Wait for response but response body is ignored, status is checked, call is sent as post.
RESTWebhook rest_webhook = 2;
// Wait for response and response body is used, status is checked, call is sent as post.
RESTCall rest_call = 3;
// 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.
RESTAsync rest_async = 4;
}
// Timeout defines the duration until ZITADEL cancels the execution.
// If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution.
google.protobuf.Duration timeout = 5 [
(validate.rules).duration = {gte: {}, lte: {seconds: 270}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
string endpoint = 6 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://example.com/hooks/ip_check\""
min_length: 1
max_length: 1000
}
];
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"name\": \"ip_allow_list\",\"restWebhook\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\"}";
};
}
message CreateTargetResponse {
// The unique identifier of the newly created target.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The timestamp of the target creation.
google.protobuf.Timestamp creation_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// Key used to sign and check payload sent to the target.
string signing_key = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message UpdateTargetRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
optional string name = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ip_allow_list\""
min_length: 1
max_length: 1000
}
];
// Defines the target type and how the response of the target is treated.
oneof target_type {
// Wait for response but response body is ignored, status is checked, call is sent as post.
RESTWebhook rest_webhook = 3;
// Wait for response and response body is used, status is checked, call is sent as post.
RESTCall rest_call = 4;
// 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.
RESTAsync rest_async = 5;
}
// Timeout defines the duration until ZITADEL cancels the execution.
// If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution.
optional google.protobuf.Duration timeout = 6 [
(validate.rules).duration = {gte: {}, lte: {seconds: 270}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
optional string endpoint = 7 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://example.com/hooks/ip_check\""
min_length: 1
max_length: 1000
}
];
// Regenerate the key used for signing and checking the payload sent to the target.
// Set the graceful period for the existing key. During that time, the previous
// signing key and the new one will be used to sign the request to allow you a smooth
// transition onf your API.
//
// Note that we currently only allow an immediate rotation ("0s") and will support
// longer expirations in the future.
optional google.protobuf.Duration expiration_signing_key = 8 [
(validate.rules).duration = {const: {seconds: 0, nanos: 0}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"0s\""
minimum: 0
maximum: 0
}
];
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"name\": \"ip_allow_list\",\"restCall\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\",\"expirationSigningKey\":\"0s\"}";
};
}
message UpdateTargetResponse {
// The timestamp of the change of the target.
google.protobuf.Timestamp change_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
// Key used to sign and check payload sent to the target.
optional string signing_key = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message DeleteTargetRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteTargetResponse {
// The timestamp of the deletion of the target.
// Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
// In case the deletion occurred in a previous request, the deletion date might be empty.
google.protobuf.Timestamp deletion_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
message GetTargetRequest {
string id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message GetTargetResponse {
Target target = 1;
}
message ListTargetsRequest {
// List limitations and ordering.
optional zitadel.filter.v2.PaginationRequest pagination = 1;
// The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent.
optional TargetFieldName sorting_column = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"TARGET_FIELD_NAME_CREATION_DATE\""
}
];
// Define the criteria to query for.
repeated TargetSearchFilter filters = 3;
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}";
};
}
message ListTargetsResponse {
reserved 'result';
zitadel.filter.v2.PaginationResponse pagination = 1;
repeated Target targets = 2;
}
message SetExecutionRequest {
// Condition defining when the execution should be used.
Condition condition = 1;
// Ordered list of targets called during the execution.
repeated string targets = 2;
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[{\"target\":\"69629026806489455\"}]}";
};
}
message SetExecutionResponse {
// The timestamp of the execution set.
google.protobuf.Timestamp set_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
}
message ListExecutionsRequest {
// List limitations and ordering.
optional zitadel.filter.v2.PaginationRequest pagination = 1;
// The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent.
optional ExecutionFieldName sorting_column = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\""
}
];
// Define the criteria to query for.
repeated ExecutionSearchFilter filters = 3;
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"EXECUTION_FIELD_NAME_ID\",\"filters\":[{\"targetFilter\":{\"targetId\":\"69629023906488334\"}}]}";
};
}
message ListExecutionsResponse {
reserved 'result';
zitadel.filter.v2.PaginationResponse pagination = 1;
repeated Execution executions = 2;
}
message ListExecutionFunctionsRequest{}
message ListExecutionFunctionsResponse{
// All available methods
repeated string functions = 1;
}
message ListExecutionMethodsRequest{}
message ListExecutionMethodsResponse{
// All available methods
repeated string methods = 1;
}
message ListExecutionServicesRequest{}
message ListExecutionServicesResponse{
// All available methods
repeated string services = 1;
}

View File

@@ -0,0 +1,135 @@
syntax = "proto3";
package zitadel.action.v2;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action";
message Execution {
Condition condition = 1;
// The timestamp of the execution creation.
google.protobuf.Timestamp creation_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The timestamp of the last change to the execution.
google.protobuf.Timestamp change_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
// Ordered list of targets called during the execution.
repeated string targets = 4;
}
message Condition {
// Condition-types under which conditions the execution should happen, only one possible.
oneof condition_type {
option (validate.required) = true;
// Condition-type to execute if a request on the defined API point happens.
RequestExecution request = 1;
// 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.
FunctionExecution function = 3;
// Condition-type to execute if an event is created in the system.
EventExecution event = 4;
}
}
message RequestExecution {
// Condition for the request execution. Only one is possible.
oneof condition{
option (validate.required) = true;
// GRPC-method as condition.
string method = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"/zitadel.session.v2.SessionService/ListSessions\"";
}
];
// GRPC-service as condition.
string service = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"zitadel.session.v2.SessionService\"";
}
];
// All calls to any available services and methods as condition.
bool all = 3 [(validate.rules).bool = {const: true}];
}
}
message ResponseExecution {
// Condition for the response execution. Only one is possible.
oneof condition{
option (validate.required) = true;
// GRPC-method as condition.
string method = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"/zitadel.session.v2.SessionService/ListSessions\"";
}
];
// GRPC-service as condition.
string service = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"zitadel.session.v2.SessionService\"";
}
];
// All calls to any available services and methods as condition.
bool all = 3 [(validate.rules).bool = {const: true}];
}
}
// 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 is possible.
oneof condition{
option (validate.required) = true;
// Event name as condition.
string event = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"user.human.added\"";
}
];
// Event group as condition, all events under this group.
string group = 2 [
(validate.rules).string = {min_len: 1, max_len: 1000},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"user.human\"";
}
];
// all events as condition.
bool all = 3 [(validate.rules).bool = {const: true}];
}
}

View File

@@ -0,0 +1,108 @@
syntax = "proto3";
package zitadel.action.v2;
option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/action/v2/execution.proto";
import "zitadel/filter/v2/filter.proto";
message ExecutionSearchFilter {
oneof filter {
option (validate.required) = true;
InConditionsFilter in_conditions_filter = 1;
ExecutionTypeFilter execution_type_filter = 2;
TargetFilter target_filter = 3;
}
}
message InConditionsFilter {
// Defines the conditions to query for.
repeated Condition conditions = 1;
}
message ExecutionTypeFilter {
// Defines the type to query for.
ExecutionType execution_type = 1;
}
message TargetFilter {
// Defines the id to query for.
string target_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the id of the targets to include"
example: "\"69629023906488334\"";
}
];
}
enum TargetFieldName {
TARGET_FIELD_NAME_UNSPECIFIED = 0;
TARGET_FIELD_NAME_ID = 1;
TARGET_FIELD_NAME_CREATED_DATE = 2;
TARGET_FIELD_NAME_CHANGED_DATE = 3;
TARGET_FIELD_NAME_NAME = 4;
TARGET_FIELD_NAME_TARGET_TYPE = 5;
TARGET_FIELD_NAME_URL = 6;
TARGET_FIELD_NAME_TIMEOUT = 7;
TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8;
}
message TargetSearchFilter {
oneof filter {
option (validate.required) = true;
TargetNameFilter target_name_filter = 1;
InTargetIDsFilter in_target_ids_filter = 2;
}
}
message TargetNameFilter {
// Defines the name of the target to query for.
string target_name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
max_length: 200;
example: "\"ip_allow_list\"";
}
];
// Defines which text comparison method used for the name query.
zitadel.filter.v2.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines which text equality method is used";
}
];
}
message InTargetIDsFilter {
// Defines the ids to query for.
repeated string target_ids = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the ids of the targets to include"
example: "[\"69629023906488334\",\"69622366012355662\"]";
}
];
}
enum ExecutionType {
EXECUTION_TYPE_UNSPECIFIED = 0;
EXECUTION_TYPE_REQUEST = 1;
EXECUTION_TYPE_RESPONSE = 2;
EXECUTION_TYPE_EVENT = 3;
EXECUTION_TYPE_FUNCTION = 4;
}
enum ExecutionFieldName {
EXECUTION_FIELD_NAME_UNSPECIFIED = 0;
EXECUTION_FIELD_NAME_ID = 1;
EXECUTION_FIELD_NAME_CREATED_DATE = 2;
EXECUTION_FIELD_NAME_CHANGED_DATE = 3;
}

View File

@@ -0,0 +1,75 @@
syntax = "proto3";
package zitadel.action.v2;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action";
message Target {
// The unique identifier of the target.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The timestamp of the target creation.
google.protobuf.Timestamp creation_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// The timestamp of the last change to the target (e.g. creation, activation, deactivation).
google.protobuf.Timestamp change_date = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
string name = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ip_allow_list\"";
}
];
// Defines the target type and how the response of the target is treated.
oneof target_type {
RESTWebhook rest_webhook = 5;
RESTCall rest_call = 6;
RESTAsync rest_async = 7;
}
// Timeout defines the duration until ZITADEL cancels the execution.
// If the target doesn't respond before this timeout expires, the the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution.
google.protobuf.Duration timeout = 8 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
string endpoint = 9 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://example.com/hooks/ip_check\""
}
];
string signing_key = 10 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message RESTWebhook {
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 1;
}
message RESTCall {
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 1;
}
message RESTAsync {}