feat: actions v2 execution targets command side (#7384)

Adds the API to create, update, delete targets for execution in a new ExecutionService (v3alpha)
This commit is contained in:
Stefan Benz 2024-02-15 06:39:10 +01:00 committed by GitHub
parent 518c8f486e
commit 198bc017b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 2552 additions and 18 deletions

View File

@ -982,6 +982,8 @@ InternalAuthZ:
- "events.read"
- "milestones.read"
- "session.delete"
- "execution.target.write"
- "execution.target.delete"
- Role: "IAM_OWNER_VIEWER"
Permissions:
- "iam.read"

View File

@ -36,6 +36,7 @@ import (
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/admin"
"github.com/zitadel/zitadel/internal/api/grpc/auth"
execution_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/execution/v3alpha"
"github.com/zitadel/zitadel/internal/api/grpc/management"
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
"github.com/zitadel/zitadel/internal/api/grpc/org/v2"
@ -400,6 +401,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
return err
}
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries)); err != nil {
return err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))

View File

@ -289,6 +289,13 @@ module.exports = {
sidebarOptions: {
groupPathsBy: "tag",
},
},
execution_v3: {
specPath: ".artifacts/openapi/zitadel/execution/v3alpha/execution_service.swagger.json",
outputDir: "docs/apis/resources/execution_service_v3",
sidebarOptions: {
groupPathsBy: "tag",
},
}
}
},

View File

@ -631,6 +631,20 @@ module.exports = {
},
items: require("./docs/apis/resources/settings_service/sidebar.js"),
},
{
type: "category",
label: "Execution Lifecycle (Alpha)",
link: {
type: "generated-index",
title: "Execution Service API (Alpha)",
slug: "/apis/resources/execution_service_v3",
description:
"This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance.\n"+
"\n"+
"This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current actions.",
},
items: require("./docs/apis/resources/execution_service_v3/sidebar.js"),
},
{
type: "category",
label: "Assets",

View File

@ -0,0 +1,51 @@
package execution
import (
"google.golang.org/grpc"
"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/query"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
)
var _ execution.ExecutionServiceServer = (*Server)(nil)
type Server struct {
execution.UnimplementedExecutionServiceServer
command *command.Commands
query *query.Queries
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
execution.RegisterExecutionServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return execution.ExecutionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return execution.ExecutionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return execution.ExecutionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return execution.RegisterExecutionServiceHandler
}

View File

@ -0,0 +1,95 @@
package execution
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
)
func (s *Server) CreateTarget(ctx context.Context, req *execution.CreateTargetRequest) (*execution.CreateTargetResponse, error) {
add := createTargetToCommand(req)
details, err := s.command.AddTarget(ctx, add, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &execution.CreateTargetResponse{
Id: add.AggregateID,
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) UpdateTarget(ctx context.Context, req *execution.UpdateTargetRequest) (*execution.UpdateTargetResponse, error) {
details, err := s.command.ChangeTarget(ctx, updateTargetToCommand(req), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &execution.UpdateTargetResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *execution.DeleteTargetRequest) (*execution.DeleteTargetResponse, error) {
details, err := s.command.DeleteTarget(ctx, req.GetTargetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &execution.DeleteTargetResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func createTargetToCommand(req *execution.CreateTargetRequest) *command.AddTarget {
var targetType domain.TargetType
var url string
switch t := req.GetTargetType().(type) {
case *execution.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
url = t.RestWebhook.GetUrl()
case *execution.CreateTargetRequest_RestRequestResponse:
targetType = domain.TargetTypeRequestResponse
url = t.RestRequestResponse.GetUrl()
}
return &command.AddTarget{
Name: req.GetName(),
TargetType: targetType,
URL: url,
Timeout: req.GetTimeout().AsDuration(),
Async: req.GetIsAsync(),
InterruptOnError: req.GetInterruptOnError(),
}
}
func updateTargetToCommand(req *execution.UpdateTargetRequest) *command.ChangeTarget {
if req == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetTargetId(),
},
Name: req.Name,
}
switch t := req.GetTargetType().(type) {
case *execution.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.URL = gu.Ptr(t.RestWebhook.GetUrl())
case *execution.UpdateTargetRequest_RestRequestResponse:
target.TargetType = gu.Ptr(domain.TargetTypeRequestResponse)
target.URL = gu.Ptr(t.RestRequestResponse.GetUrl())
}
if req.Timeout != nil {
target.Timeout = gu.Ptr(req.GetTimeout().AsDuration())
}
if req.ExecutionType != nil {
target.Async = gu.Ptr(req.GetIsAsync())
target.InterruptOnError = gu.Ptr(req.GetInterruptOnError())
}
return target
}

View File

@ -0,0 +1,411 @@
//go:build integration
package execution_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
var (
CTX context.Context
Tester *integration.Tester
Client execution.ExecutionServiceClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.ExecutionV3
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
return m.Run()
}())
}
func TestServer_CreateTarget(t *testing.T) {
tests := []struct {
name string
ctx context.Context
req *execution.CreateTargetRequest
want *execution.CreateTargetResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &execution.SetRESTRequestResponse{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: nil,
ExecutionType: nil,
},
wantErr: true,
},
{
name: "empty execution type, ok",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: nil,
},
want: &execution.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "async execution, ok",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.CreateTargetRequest_IsAsync{
IsAsync: true,
},
},
want: &execution.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "interrupt on error execution, ok",
ctx: CTX,
req: &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
},
want: &execution.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
assert.NotEmpty(t, got.GetId())
})
}
}
func TestServer_UpdateTarget(t *testing.T) {
type args struct {
ctx context.Context
req *execution.UpdateTargetRequest
}
tests := []struct {
name string
prepare func(request *execution.UpdateTargetRequest) error
args args
want *execution.UpdateTargetResponse
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *execution.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &execution.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *execution.UpdateTargetRequest) error {
request.TargetId = "notexisting"
return nil
},
args: args{
ctx: CTX,
req: &execution.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *execution.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &execution.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
want: &execution.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type, ok",
prepare: func(request *execution.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &execution.UpdateTargetRequest{
TargetType: &execution.UpdateTargetRequest_RestRequestResponse{
RestRequestResponse: &execution.SetRESTRequestResponse{
Url: "https://example.com",
},
},
},
},
want: &execution.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change url, ok",
prepare: func(request *execution.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &execution.UpdateTargetRequest{
TargetType: &execution.UpdateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com/hooks/new",
},
},
},
},
want: &execution.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change timeout, ok",
prepare: func(request *execution.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &execution.UpdateTargetRequest{
Timeout: durationpb.New(20 * time.Second),
},
},
want: &execution.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change execution type, ok",
prepare: func(request *execution.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &execution.UpdateTargetRequest{
ExecutionType: &execution.UpdateTargetRequest_IsAsync{
IsAsync: true,
},
},
},
want: &execution.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.UpdateTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
target := Tester.CreateTarget(CTX, t)
tests := []struct {
name string
ctx context.Context
req *execution.DeleteTargetRequest
want *execution.DeleteTargetResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &execution.DeleteTargetRequest{
TargetId: target.GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: CTX,
req: &execution.DeleteTargetRequest{
TargetId: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: CTX,
req: &execution.DeleteTargetRequest{
TargetId: target.GetId(),
},
want: &execution.DeleteTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,193 @@
package execution
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"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *execution.CreateTargetRequest
}
tests := []struct {
name string
args args
want *command.AddTarget
}{
{
name: "nil",
args: args{nil},
want: &command.AddTarget{
Name: "",
TargetType: domain.TargetTypeUnspecified,
URL: "",
Timeout: 0,
Async: false,
InterruptOnError: false,
},
},
{
name: "all fields (async webhook)",
args: args{&execution.CreateTargetRequest{
Name: "target 1",
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com/hooks/1",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.CreateTargetRequest_IsAsync{
IsAsync: true,
},
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
URL: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
Async: true,
InterruptOnError: false,
},
},
{
name: "all fields (interrupting response)",
args: args{&execution.CreateTargetRequest{
Name: "target 1",
TargetType: &execution.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &execution.SetRESTRequestResponse{
Url: "https://example.com/hooks/1",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeRequestResponse,
URL: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
Async: false,
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 *execution.UpdateTargetRequest
}
tests := []struct {
name string
args args
want *command.ChangeTarget
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields nil",
args: args{&execution.UpdateTargetRequest{
Name: nil,
TargetType: nil,
Timeout: nil,
ExecutionType: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
URL: nil,
Timeout: nil,
Async: nil,
InterruptOnError: nil,
},
},
{
name: "all fields empty",
args: args{&execution.UpdateTargetRequest{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
ExecutionType: nil,
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
URL: nil,
Timeout: gu.Ptr(0 * time.Second),
Async: nil,
InterruptOnError: nil,
},
},
{
name: "all fields (async webhook)",
args: args{&execution.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
TargetType: &execution.UpdateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com/hooks/1",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.UpdateTargetRequest_IsAsync{
IsAsync: true,
},
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
URL: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(true),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (interrupting response)",
args: args{&execution.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
TargetType: &execution.UpdateTargetRequest_RestRequestResponse{
RestRequestResponse: &execution.SetRESTRequestResponse{
Url: "https://example.com/hooks/1",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.UpdateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeRequestResponse),
URL: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(false),
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

@ -0,0 +1,177 @@
package command
import (
"context"
"net/url"
"time"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/target"
"github.com/zitadel/zitadel/internal/zerrors"
)
type AddTarget struct {
models.ObjectRoot
Name string
TargetType domain.TargetType
URL string
Timeout time.Duration
Async bool
InterruptOnError bool
}
func (a *AddTarget) IsValid() error {
if a.Name == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-ddqbm9us5p", "Errors.Target.Invalid")
}
if a.Timeout == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-39f35d8uri", "Errors.Target.NoTimeout")
}
_, err := url.Parse(a.URL)
if err != nil || a.URL == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
}
return nil
}
func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing")
}
if err := add.IsValid(); err != nil {
return nil, err
}
if add.AggregateID == "" {
add.AggregateID, err = c.idGenerator.Next()
if err != nil {
return nil, err
}
}
wm := NewTargetWriteModel(add.AggregateID, resourceOwner)
pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent(
ctx,
TargetAggregateFromWriteModel(&wm.WriteModel),
add.Name,
add.TargetType,
add.URL,
add.Timeout,
add.Async,
add.InterruptOnError,
))
if err != nil {
return nil, err
}
if err := AppendAndReduce(wm, pushedEvents...); err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
type ChangeTarget struct {
models.ObjectRoot
Name *string
TargetType *domain.TargetType
URL *string
Timeout *time.Duration
Async *bool
InterruptOnError *bool
}
func (a *ChangeTarget) IsValid() error {
if a.AggregateID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-1l6ympeagp", "Errors.IDMissing")
}
if a.Name != nil && *a.Name == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-d1wx4lm0zr", "Errors.Target.Invalid")
}
if a.Timeout != nil && *a.Timeout == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-08b39vdi57", "Errors.Target.NoTimeout")
}
if a.URL != nil {
_, err := url.Parse(*a.URL)
if err != nil || *a.URL == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
}
}
return nil
}
func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resourceOwner string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing")
}
if err := change.IsValid(); err != nil {
return nil, err
}
existing, err := c.getTargetWriteModelByID(ctx, change.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
if !existing.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound")
}
changedEvent := existing.NewChangedEvent(
ctx,
TargetAggregateFromWriteModel(&existing.WriteModel),
change.Name,
change.TargetType,
change.URL,
change.Timeout,
change.Async,
change.InterruptOnError)
if changedEvent == nil {
return writeModelToObjectDetails(&existing.WriteModel), nil
}
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(existing, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existing.WriteModel), nil
}
func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) (*domain.ObjectDetails, error) {
if id == "" || resourceOwner == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing")
}
existing, err := c.getTargetWriteModelByID(ctx, id, resourceOwner)
if err != nil {
return nil, err
}
if !existing.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-k4s7ucu0ax", "Errors.Target.NotFound")
}
if err := c.pushAppendAndReduce(ctx,
existing,
target.NewRemovedEvent(ctx,
TargetAggregateFromWriteModel(&existing.WriteModel),
existing.Name,
),
); err != nil {
return nil, err
}
return writeModelToObjectDetails(&existing.WriteModel), nil
}
func (c *Commands) getTargetWriteModelByID(ctx context.Context, id string, resourceOwner string) (*TargetWriteModel, error) {
wm := NewTargetWriteModel(id, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)
if err != nil {
return nil, err
}
return wm, nil
}

View File

@ -0,0 +1,127 @@
package command
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/action"
"github.com/zitadel/zitadel/internal/repository/target"
)
type TargetWriteModel struct {
eventstore.WriteModel
Name string
TargetType domain.TargetType
URL string
Timeout time.Duration
Async bool
InterruptOnError bool
State domain.TargetState
}
func NewTargetWriteModel(id string, resourceOwner string) *TargetWriteModel {
return &TargetWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: id,
ResourceOwner: resourceOwner,
InstanceID: resourceOwner,
},
}
}
func (wm *TargetWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *target.AddedEvent:
wm.Name = e.Name
wm.TargetType = e.TargetType
wm.URL = e.URL
wm.Timeout = e.Timeout
wm.Async = e.Async
wm.State = domain.TargetActive
case *target.ChangedEvent:
if e.Name != nil {
wm.Name = *e.Name
}
if e.TargetType != nil {
wm.TargetType = *e.TargetType
}
if e.URL != nil {
wm.URL = *e.URL
}
if e.Timeout != nil {
wm.Timeout = *e.Timeout
}
if e.Async != nil {
wm.Async = *e.Async
}
if e.InterruptOnError != nil {
wm.InterruptOnError = *e.InterruptOnError
}
case *action.RemovedEvent:
wm.State = domain.TargetRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *TargetWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(target.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(target.AddedEventType,
target.ChangedEventType,
target.RemovedEventType).
Builder()
}
func (wm *TargetWriteModel) NewChangedEvent(
ctx context.Context,
agg *eventstore.Aggregate,
name *string,
targetType *domain.TargetType,
url *string,
timeout *time.Duration,
async *bool,
interruptOnError *bool,
) *target.ChangedEvent {
changes := make([]target.Changes, 0)
if name != nil && wm.Name != *name {
changes = append(changes, target.ChangeName(wm.Name, *name))
}
if targetType != nil && wm.TargetType != *targetType {
changes = append(changes, target.ChangeTargetType(*targetType))
}
if url != nil && wm.URL != *url {
changes = append(changes, target.ChangeURL(*url))
}
if timeout != nil && wm.Timeout != *timeout {
changes = append(changes, target.ChangeTimeout(*timeout))
}
if async != nil && wm.Async != *async {
changes = append(changes, target.ChangeAsync(*async))
}
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
changes = append(changes, target.ChangeInterruptOnError(*interruptOnError))
}
if len(changes) == 0 {
return nil
}
return target.NewChangedEvent(ctx, agg, changes)
}
func TargetAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,
Type: target.AggregateType,
ResourceOwner: wm.ResourceOwner,
InstanceID: wm.InstanceID,
Version: target.AggregateVersion,
}
}

View File

@ -0,0 +1,676 @@
package command
import (
"context"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/target"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommands_AddTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
ctx context.Context
add *AddTarget
resourceOwner string
}
type res struct {
id string
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"no resourceowner, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
add: &AddTarget{},
resourceOwner: "",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no name, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
add: &AddTarget{},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no timeout, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no url, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
Timeout: time.Second,
URL: "",
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no parsable url, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
Timeout: time.Second,
URL: "://",
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
URL: "https://example.com",
Timeout: time.Second,
TargetType: domain.TargetTypeWebhook,
},
resourceOwner: "org1",
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
URL: "https://example.com",
},
resourceOwner: "org1",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
"push full ok",
fields{
eventstore: eventstoreExpect(t,
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
true,
true,
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
TargetType: domain.TargetTypeWebhook,
URL: "https://example.com",
Timeout: time.Second,
Async: true,
InterruptOnError: true,
},
resourceOwner: "org1",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
}
details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.id, tt.args.add.AggregateID)
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_ChangeTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
change *ChangeTarget
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"resourceowner missing, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
change: &ChangeTarget{},
resourceOwner: "",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
change: &ChangeTarget{},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"name empty, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
Name: gu.Ptr(""),
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"timeout empty, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
Timeout: gu.Ptr(time.Duration(0)),
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"url empty, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
URL: gu.Ptr(""),
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"url not parsable, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
URL: gu.Ptr("://"),
},
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: gu.Ptr("name"),
},
resourceOwner: "org1",
},
res{
err: zerrors.IsNotFound,
},
},
{
"no changes",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
),
),
),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
TargetType: gu.Ptr(domain.TargetTypeWebhook),
},
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
),
),
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
[]target.Changes{
target.ChangeName("name", "name2"),
},
),
),
),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: gu.Ptr("name2"),
},
resourceOwner: "org1",
},
res{
err: zerrors.IsPreconditionFailed,
},
},
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
),
),
expectPush(
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
[]target.Changes{
target.ChangeName("name", "name2"),
},
),
),
),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: gu.Ptr("name2"),
},
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
"push full ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
),
),
expectPush(
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
[]target.Changes{
target.ChangeName("name", "name2"),
target.ChangeURL("https://example2.com"),
target.ChangeTargetType(domain.TargetTypeRequestResponse),
target.ChangeTimeout(time.Second),
target.ChangeAsync(true),
target.ChangeInterruptOnError(true),
},
),
),
),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: gu.Ptr("name2"),
URL: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeRequestResponse),
Timeout: gu.Ptr(time.Second),
Async: gu.Ptr(true),
InterruptOnError: gu.Ptr(true),
},
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}
func TestCommands_DeleteTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
id string
resourceOwner string
}
type res struct {
details *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
id: "",
resourceOwner: "org1",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
},
res{
err: zerrors.IsNotFound,
},
},
{
"remove ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
),
),
expectPush(
target.NewRemovedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
),
),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
}
details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.details, details)
}
})
}
}

28
internal/domain/target.go Normal file
View File

@ -0,0 +1,28 @@
package domain
type TargetType uint
const (
TargetTypeUnspecified TargetType = iota
TargetTypeWebhook
TargetTypeRequestResponse
TargetTypeStateCount
)
type TargetState int32
const (
TargetUnspecified TargetState = iota
TargetActive
TargetRemoved
targetStateCount
)
func (s TargetState) Valid() bool {
return s >= 0 && s < targetStateCount
}
func (s TargetState) Exists() bool {
return s != TargetUnspecified && s != TargetRemoved
}

View File

@ -26,6 +26,7 @@ import (
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/auth"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
@ -38,28 +39,30 @@ import (
)
type Client struct {
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2 organisation.OrganizationServiceClient
System system.SystemServiceClient
CC *grpc.ClientConn
Admin admin.AdminServiceClient
Mgmt mgmt.ManagementServiceClient
Auth auth.AuthServiceClient
UserV2 user.UserServiceClient
SessionV2 session.SessionServiceClient
OIDCv2 oidc_pb.OIDCServiceClient
OrgV2 organisation.OrganizationServiceClient
System system.SystemServiceClient
ExecutionV3 execution.ExecutionServiceClient
}
func newClient(cc *grpc.ClientConn) Client {
return Client{
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2: organisation.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
CC: cc,
Admin: admin.NewAdminServiceClient(cc),
Mgmt: mgmt.NewManagementServiceClient(cc),
Auth: auth.NewAuthServiceClient(cc),
UserV2: user.NewUserServiceClient(cc),
SessionV2: session.NewSessionServiceClient(cc),
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
OrgV2: organisation.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
ExecutionV3: execution.NewExecutionServiceClient(cc),
}
}
@ -503,3 +506,20 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj
})
require.NoError(t, err)
}
func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *execution.CreateTargetResponse {
target, err := s.Client.ExecutionV3.CreateTarget(ctx, &execution.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &execution.CreateTargetRequest_RestWebhook{
RestWebhook: &execution.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &execution.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
})
require.NoError(t, err)
return target
}

View File

@ -0,0 +1,18 @@
package target
import "github.com/zitadel/zitadel/internal/eventstore"
const (
AggregateType = "target"
AggregateVersion = "v1"
)
func NewAggregate(aggrID, instanceID string) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: aggrID,
Type: AggregateType,
ResourceOwner: instanceID,
InstanceID: instanceID,
Version: AggregateVersion,
}
}

View File

@ -0,0 +1,25 @@
package target
import (
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
UniqueTarget = "target"
DuplicateTarget = "Errors.Target.AlreadyExists"
)
func NewAddUniqueConstraint(name string) *eventstore.UniqueConstraint {
return eventstore.NewAddEventUniqueConstraint(
UniqueTarget,
name,
DuplicateTarget,
)
}
func NewRemoveUniqueConstraint(name string) *eventstore.UniqueConstraint {
return eventstore.NewRemoveUniqueConstraint(
UniqueTarget,
name,
)
}

View File

@ -0,0 +1,9 @@
package target
import "github.com/zitadel/zitadel/internal/eventstore"
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, AddedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, ChangedEventType, ChangedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, RemovedEventMapper)
}

View File

@ -0,0 +1,199 @@
package target
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
eventTypePrefix eventstore.EventType = "target."
AddedEventType = eventTypePrefix + "added"
ChangedEventType = eventTypePrefix + "changed"
RemovedEventType = eventTypePrefix + "removed"
)
type AddedEvent struct {
*eventstore.BaseEvent `json:"-"`
Name string `json:"name"`
TargetType domain.TargetType `json:"targetType"`
URL string `json:"url"`
Timeout time.Duration `json:"timeout"`
Async bool `json:"async"`
InterruptOnError bool `json:"interruptOnError"`
}
func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *AddedEvent) Payload() any {
return e
}
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewAddUniqueConstraint(e.Name)}
}
func NewAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
name string,
targetType domain.TargetType,
url string,
timeout time.Duration,
async bool,
interruptOnError bool,
) *AddedEvent {
return &AddedEvent{
eventstore.NewBaseEventForPush(
ctx, aggregate, AddedEventType,
),
name, targetType, url, timeout, async, interruptOnError}
}
func AddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
added := &AddedEvent{
BaseEvent: eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(added)
if err != nil {
return nil, zerrors.ThrowInternal(err, "TARGET-fx8f8yfbn1", "unable to unmarshal target added")
}
return added, nil
}
type ChangedEvent struct {
*eventstore.BaseEvent `json:"-"`
Name *string `json:"name,omitempty"`
TargetType *domain.TargetType `json:"targetType,omitempty"`
URL *string `json:"url,omitempty"`
Timeout *time.Duration `json:"timeout,omitempty"`
Async *bool `json:"async,omitempty"`
InterruptOnError *bool `json:"interruptOnError,omitempty"`
oldName string
}
func (e *ChangedEvent) Payload() interface{} {
return e
}
func (e *ChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
if e.oldName == "" {
return nil
}
return []*eventstore.UniqueConstraint{
NewRemoveUniqueConstraint(e.oldName),
NewAddUniqueConstraint(*e.Name),
}
}
func NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []Changes,
) *ChangedEvent {
changeEvent := &ChangedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
ChangedEventType,
),
}
for _, change := range changes {
change(changeEvent)
}
return changeEvent
}
type Changes func(event *ChangedEvent)
func ChangeName(oldName, name string) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Name = &name
e.oldName = oldName
}
}
func ChangeTargetType(targetType domain.TargetType) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.TargetType = &targetType
}
}
func ChangeURL(url string) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.URL = &url
}
}
func ChangeTimeout(timeout time.Duration) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Timeout = &timeout
}
}
func ChangeAsync(async bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Async = &async
}
}
func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.InterruptOnError = &interruptOnError
}
}
func ChangedEventMapper(event eventstore.Event) (eventstore.Event, error) {
changed := &ChangedEvent{
BaseEvent: eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(changed)
if err != nil {
return nil, zerrors.ThrowInternal(err, "TARGET-w6402p4ek7", "unable to unmarshal target changed")
}
return changed, nil
}
type RemovedEvent struct {
*eventstore.BaseEvent `json:"-"`
name string
}
func (e *RemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *RemovedEvent) Payload() any {
return e
}
func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{NewRemoveUniqueConstraint(e.name)}
}
func NewRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, name string) *RemovedEvent {
return &RemovedEvent{eventstore.NewBaseEventForPush(ctx, aggregate, RemovedEventType), name}
}
func RemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
removed := &RemovedEvent{
BaseEvent: eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(removed)
if err != nil {
return nil, zerrors.ThrowInternal(err, "TARGET-0kuc12c7bc", "unable to unmarshal target removed")
}
return removed, nil
}

View File

@ -552,6 +552,11 @@ Errors:
NotExisting: Функцията не съществува
TypeNotSupported: Типът функция не се поддържа
InvalidValue: Невалидна стойност за тази функция
Target:
Invalid: Целта е невалидна
NoTimeout: Целта няма време за изчакване
InvalidURL: Целта има невалиден URL адрес
NotFound: Целта не е намерена
AggregateTypes:
action: Действие
@ -563,8 +568,13 @@ AggregateTypes:
usergrant: Предоставяне на потребител
quota: Квота
feature: Особеност
target: Целта
EventTypes:
target:
added: Целта е създадена
changed: Целта е променена
removed: Целта е изтрита
user:
added: Добавен потребител
selfregistered: Потребителят се регистрира сам

View File

@ -532,6 +532,11 @@ Errors:
NotExisting: Funkce neexistuje
TypeNotSupported: Typ funkce není podporován
InvalidValue: Neplatná hodnota pro tuto funkci
Target:
Invalid: Cíl je neplatný
NoTimeout: Cíl nemá časový limit
InvalidURL: Cíl má neplatnou adresu URL
NotFound: Cíl nenalezen
AggregateTypes:
action: Akce
@ -543,8 +548,13 @@ AggregateTypes:
usergrant: Uživatelský grant
quota: Kvóta
feature: Funkce
target: Cíl
EventTypes:
target:
added: Cíl vytvořen
changed: Cíl změněn
removed: Cíl smazán
user:
added: Uživatel přidán
selfregistered: Uživatel se zaregistroval sám

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: Feature existiert nicht
TypeNotSupported: Feature Typ wird nicht unterstützt
InvalidValue: Ungültiger Wert für dieses Feature
Target:
Invalid: Ziel ist ungültig
NoTimeout: Ziel hat keinen Timeout
InvalidURL: Ziel hat eine ungültige URL
NotFound: Ziel nicht gefunden
AggregateTypes:
action: Action
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: Benutzerberechtigung
quota: Kontingent
feature: Feature
target: Ziel
EventTypes:
target:
added: Ziel erstellt
changed: Ziel geändert
removed: Ziel gelöscht
user:
added: Benutzer hinzugefügt
selfregistered: Benutzer hat sich selbst registriert

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: Feature does not exist
TypeNotSupported: Feature type is not supported
InvalidValue: Invalid value for this feature
Target:
Invalid: Target is invalid
NoTimeout: Target has no timeout
InvalidURL: Target has an invalid URL
NotFound: Target not found
AggregateTypes:
action: Action
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: User grant
quota: Quota
feature: Feature
target: Target
EventTypes:
target:
added: Target created
changed: Target changed
removed: Target deleted
user:
added: User added
selfregistered: User registered himself

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: La característica no existe
TypeNotSupported: El tipo de característica no es compatible
InvalidValue: Valor no válido para esta característica
Target:
Invalid: El objetivo no es válido
NoTimeout: El objetivo no tiene tiempo de espera
InvalidURL: El objetivo tiene una URL no válida
NotFound: El objetivo no encontrado
AggregateTypes:
action: Acción
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: Concesión de usuario
quota: Cuota
feature: Característica
target: Objectivo
EventTypes:
target:
added: Objetivo creado
changed: Objetivo cambiado
removed: Objetivo eliminado
user:
added: Usuario añadido
selfregistered: El usuario se registró por sí mismo

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: La fonctionnalité n'existe pas
TypeNotSupported: Le type de fonctionnalité n'est pas pris en charge
InvalidValue: Valeur non valide pour cette fonctionnalité
Target:
Invalid: La cible n'est pas valide
NoTimeout: La cible n'a pas de délai d'attente
InvalidURL: La cible a une URL non valide
NotFound: La cible introuvable
AggregateTypes:
action: Action
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: Subvention de l'utilisateur
quota: Contingent
feature: Fonctionnalité
target: Cible
EventTypes:
target:
added: Cible créée
changed: Cible modifiée
removed: Cible supprimée
user:
added: Utilisateur ajouté
selfregistered: L'utilisateur s'est enregistré lui-même

View File

@ -536,6 +536,11 @@ Errors:
NotExisting: La funzionalità non esiste
TypeNotSupported: Il tipo di funzionalità non è supportato
InvalidValue: Valore non valido per questa funzionalità
Target:
Invalid: Il target non è valido
NoTimeout: Il target non ha timeout
InvalidURL: La destinazione ha un URL non valido
NotFound: Obiettivo non trovato
AggregateTypes:
action: Azione
@ -547,8 +552,13 @@ AggregateTypes:
usergrant: Sovvenzione utente
quota: Quota
feature: Funzionalità
target: Bersaglio
EventTypes:
target:
added: Obiettivo creato
changed: Obiettivo cambiato
removed: Obiettivo eliminato
user:
added: Utente aggiunto
selfregistered: L'utente si è registrato

View File

@ -524,6 +524,11 @@ Errors:
NotExisting: 機能が存在しません
TypeNotSupported: 機能タイプはサポートされていません
InvalidValue: この機能には無効な値です
Target:
Invalid: ターゲットが無効です
NoTimeout: ターゲットにはタイムアウトがありません
InvalidURL: ターゲットに無効な URL があります
NotFound: ターゲットが見つかりません
AggregateTypes:
action: アクション
@ -535,8 +540,13 @@ AggregateTypes:
usergrant: ユーザーグラント
quota: クォータ
feature: 特徴
target: 目標
EventTypes:
target:
added: ターゲットが作成されました
changed: ターゲットが変更されました
removed: ターゲットが削除されました
user:
added: ユーザーの追加
selfregistered: ユーザー自身の登録

View File

@ -534,6 +534,11 @@ Errors:
NotExisting: Функцијата не постои
TypeNotSupported: Типот на функција не е поддржан
InvalidValue: Неважечка вредност за оваа функција
Target:
Invalid: Целта е неважечка
NoTimeout: Целта нема тајмаут
InvalidURL: Целта има неважечка URL-адреса
NotFound: Целта не е пронајдена
AggregateTypes:
action: Акција
@ -545,8 +550,13 @@ AggregateTypes:
usergrant: Овластување на корисник
quota: Квота
feature: Карактеристика
target: Цел
EventTypes:
target:
added: Целта е избришана
changed: Целта е променета
removed: Целта е избришана
user:
added: Додаден корисник
selfregistered: Корисникот се регистрираше сам

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: Functie bestaat niet
TypeNotSupported: Functie type wordt niet ondersteund
InvalidValue: Ongeldige waarde voor deze functie
Target:
Invalid: Doel is ongeldig
NoTimeout: Doel heeft geen time-out
InvalidURL: Doel heeft een ongeldige URL
NotFound: Doel niet gevonden
AggregateTypes:
action: Actie
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: Gebruikerstoekenning
quota: Quota
feature: Functie
target: Doel
EventTypes:
target:
added: Doel gemaakt
changed: Doel gewijzigd
removed: Doel verwijderd
user:
added: Gebruiker toegevoegd
selfregistered: Gebruiker heeft zichzelf geregistreerd

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: Funkcja nie istnieje
TypeNotSupported: Typ funkcji nie jest obsługiwany
InvalidValue: Nieprawidłowa wartość dla tej funkcji
Target:
Invalid: Cel jest nieprawidłowy
NoTimeout: Cel nie ma limitu czasu
InvalidURL: Cel ma nieprawidłowy adres URL
NotFound: Nie znaleziono celu
AggregateTypes:
action: Działanie
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: Uprawnienie użytkownika
quota: Limit
feature: Funkcja
target: Cel
EventTypes:
target:
added: Cel został utworzony
changed: Cel zmieniony
removed: Cel usunięty
user:
added: Użytkownik dodany
selfregistered: Użytkownik zarejestrował się

View File

@ -529,6 +529,11 @@ Errors:
NotExisting: O recurso não existe
TypeNotSupported: O tipo de recurso não é compatível
InvalidValue: Valor inválido para este recurso
Target:
Invalid: A meta é inválida
NoTimeout: O destino não tem tempo limite
InvalidURL: O destino tem um URL inválido
NotFound: Destino não encontrado
AggregateTypes:
action: Ação
@ -540,8 +545,13 @@ AggregateTypes:
usergrant: Concessão de usuário
quota: Cota
feature: Recurso
target: objetivo
EventTypes:
target:
added: Destino criado
changed: Destino alterada
removed: Destino excluído
user:
added: Usuário adicionado
selfregistered: Usuário se registrou

View File

@ -519,6 +519,16 @@ Errors:
Invalid: Токен недействителен
Expired: Срок действия токена истек
InvalidClient: Токен не был выпущен для этого клиента
Feature:
NotExisting: ункция не существует
TypeNotSupported: Тип объекта не поддерживается
InvalidValue: Недопустимое значение для этой функции.
Target:
Invalid: Цель недействительна.
NoTimeout: У цели нет тайм-аута
InvalidURL: Цель имеет неверный URL-адрес
NotFound: Цель не найдена
AggregateTypes:
action: Действие
instance: Пример
@ -528,7 +538,14 @@ AggregateTypes:
user: Пользователь
usergrant: Разрешение пользователя
quota: Квота
feature: Особенность
target: мишень
EventTypes:
target:
added: Цель создана
changed: Цель изменена
removed: Цель удалена.
user:
added: Добавлено пользователем
selfregistered: Пользователь зарегистрировался сам

View File

@ -535,6 +535,11 @@ Errors:
NotExisting: 功能不存在
TypeNotSupported: 不支持功能类型
InvalidValue: 此功能的值无效
Target:
Invalid: 目标无效
NoTimeout: 目标没有超时
InvalidURL: 目标的 URL 无效
NotFound: 未找到目标
AggregateTypes:
action: 动作
@ -546,8 +551,13 @@ AggregateTypes:
usergrant: 用户授权
quota: 配额
feature: 特征
target:
EventTypes:
target:
added: 目标已创建
changed: 目标改变
removed: 目标已删除
user:
added: 已添加用户
selfregistered: 自注册用户

View File

@ -0,0 +1,293 @@
syntax = "proto3";
package zitadel.execution.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/execution/v3alpha/target.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha;execution";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Execution Service";
version: "3.0-alpha";
description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current actions.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
consumes: "application/grpc";
produces: "application/json";
produces: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
host: "$CUSTOM-DOMAIN";
base_path: "/";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the resource.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
service ExecutionService {
// Create a target
//
// Create a new target, which can be used in executions.
rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) {
option (google.api.http) = {
post: "/v3alpha/targets"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.target.write"
}
http_response: {
success_code: 201
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "201";
value: {
description: "Target successfully created";
schema: {
json_schema: {
ref: "#/definitions/v3alphaCreateTargetResponse";
}
}
};
};
};
}
// Update a target
//
// Update an existing target.
rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) {
option (google.api.http) = {
put: "/v3alpha/targets/{target_id}"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.target.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target successfully updated";
};
};
};
}
// Delete a target
//
// Delete an existing target. This will remove it from any configured execution as well.
rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) {
option (google.api.http) = {
delete: "/v3alpha/targets/{target_id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.target.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Target successfully deleted";
};
};
};
}
}
message CreateTargetRequest {
// Unique name of the target.
string name = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"ip_allow_list\"";
}
];
// Defines the target type and how the response of the target is treated.
oneof target_type {
option (validate.required) = true;
SetRESTWebhook rest_webhook = 2;
SetRESTRequestResponse rest_request_response = 3;
}
// Timeout defines the duration until ZITADEL cancels the execution.
google.protobuf.Duration timeout = 4 [
(validate.rules).duration = {gt: {seconds: 0}, required: true},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 5;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 6;
}
}
message CreateTargetResponse {
// ID is the read-only unique identifier of the target.
string id = 1;
// Details provide some base information (such as the last change date) of the target.
zitadel.object.v2beta.Details details = 2;
}
message UpdateTargetRequest {
// unique identifier of the target.
string target_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
// Optionally change the unique name of the target.
optional string name = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"ip_allow_list\"";
}
];
// Optionally change the target type and how the response of the target is treated,
// or its target URL.
oneof target_type {
SetRESTWebhook rest_webhook = 3;
SetRESTRequestResponse rest_request_response = 4;
}
// Optionally change the timeout, which defines the duration until ZITADEL cancels the execution.
optional google.protobuf.Duration timeout = 5 [
(validate.rules).duration = {gt: {seconds: 0}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"10s\"";
}
];
oneof execution_type {
// Set the execution to run asynchronously.
bool is_async = 6;
// Define if any error stops the whole execution. By default the process continues as normal.
bool interrupt_on_error = 7;
}
}
message UpdateTargetResponse {
// Details provide some base information (such as the last change date) of the target.
zitadel.object.v2beta.Details details = 1;
}
message DeleteTargetRequest {
// unique identifier of the target.
string target_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteTargetResponse {
// Details provide some base information (such as the last change date) of the target.
zitadel.object.v2beta.Details details = 1;
}

View File

@ -0,0 +1,38 @@
syntax = "proto3";
package zitadel.execution.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha;execution";
message SetRESTWebhook {
string url = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000, uri: true},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"https://example.com/hooks/ip_check\"";
}
];
}
message SetRESTRequestResponse {
string url = 1 [
(validate.rules).string = {min_len: 1, max_len: 1000, uri: true},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 1000,
example: "\"https://example.com/hooks/ip_check\"";
}
];
}