feat: add executions for actions v2 (#7433)

* feat: add events for execution

* feat: add events for execution and command side

* feat: add events for execution and command side

* feat: add api endpoints for set and delete executions with integration tests

* feat: add integration and unit tests and more existence checks

* feat: add integration and unit tests and more existence checks

* feat: unit tests for includes in executions

* feat: integration tests for includes in executions

* fix: linting

* fix: update internal/api/api.go

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>

* fix: update internal/command/command.go

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>

* fix: apply suggestions from code review

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>

* fix: change api return

* fix: change aggregateID with prefix of execution type and add to documentation

* fix: change body in proto for documentation and correct linting

* fix: changed existing check to single query in separate writemodel

* fix: linter changes and list endpoints for conditions in executions

* fix: remove writemodel query on exeuction set as state before is irrelevant

* fix: testing for exists write models and correction

* fix: translations for errors and event types

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Stefan Benz
2024-02-26 11:49:43 +01:00
committed by GitHub
parent ce7ebffa84
commit 2731099db3
39 changed files with 5893 additions and 58 deletions

View File

@@ -998,6 +998,9 @@ InternalAuthZ:
- "session.delete"
- "execution.target.write"
- "execution.target.delete"
- "execution.read"
- "execution.write"
- "execution.delete"
- Role: "IAM_OWNER_VIEWER"
Permissions:
- "iam.read"

View File

@@ -9,6 +9,7 @@ import (
"net/http"
"os"
"os/signal"
"slices"
"syscall"
"time"
@@ -264,7 +265,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
if err != nil {
return err
}
err = startAPIs(
api, err := startAPIs(
ctx,
clock,
router,
@@ -281,6 +282,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
if err != nil {
return err
}
commands.GrpcMethodExisting = checkExisting(api.ListGrpcMethods())
commands.GrpcServiceExisting = checkExisting(api.ListGrpcServices())
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
@@ -319,7 +322,7 @@ func startAPIs(
authZRepo authz_repo.Repository,
keys *encryption.EncryptionKeys,
permissionCheck domain.PermissionCheck,
) error {
) (*api.API, error) {
repo := struct {
authz_repo.Repository
*query.Queries
@@ -332,22 +335,22 @@ func startAPIs(
router.Use(middleware.WithOrigin(config.ExternalSecure))
systemTokenVerifier, err := internal_authz.StartSystemTokenVerifierFromConfig(http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers)
if err != nil {
return err
return nil, err
}
accessTokenVerifer := internal_authz.StartAccessTokenVerifierFromRepo(repo)
verifier := internal_authz.StartAPITokenVerifier(repo, accessTokenVerifer, systemTokenVerifier)
tlsConfig, err := config.TLS.Config()
if err != nil {
return err
return nil, err
}
accessStdoutEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &logstore.EmitterConfig{Enabled: config.LogStore.Access.Stdout.Enabled}, stdout.NewStdoutEmitter[*record.AccessLog]())
if err != nil {
return err
return nil, err
}
accessDBEmitter, err := logstore.NewEmitter[*record.AccessLog](ctx, clock, &config.Quotas.Access.EmitterConfig, access.NewDatabaseLogStorage(dbClient, commands, queries))
if err != nil {
return err
return nil, err
}
accessSvc := logstore.New[*record.AccessLog](queries, accessDBEmitter, accessStdoutEmitter)
@@ -359,50 +362,50 @@ func startAPIs(
limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig)
apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, limitingAccessInterceptor)
if err != nil {
return fmt.Errorf("error creating api %w", err)
return nil, fmt.Errorf("error creating api %w", err)
}
config.Auth.Spooler.Client = dbClient
config.Auth.Spooler.Eventstore = eventstore
authRepo, err := auth_es.Start(ctx, config.Auth, config.SystemDefaults, commands, queries, dbClient, eventstore, keys.OIDC, keys.User)
if err != nil {
return fmt.Errorf("error starting auth repo: %w", err)
return nil, fmt.Errorf("error starting auth repo: %w", err)
}
config.Admin.Spooler.Client = dbClient
config.Admin.Spooler.Eventstore = eventstore
err = admin_es.Start(ctx, config.Admin, store, dbClient)
if err != nil {
return fmt.Errorf("error starting admin repo: %w", err)
return nil, fmt.Errorf("error starting admin repo: %w", err)
}
if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil {
return err
return nil, err
}
if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, config.SystemDefaults, config.ExternalSecure, keys.User, config.AuditLogRetention), tlsConfig); err != nil {
return err
return nil, err
}
if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil {
return err
return nil, err
}
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure), tlsConfig); err != nil {
return err
return nil, err
}
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(config.ExternalSecure), idp.SAMLRootURL(config.ExternalSecure), assets.AssetAPI(config.ExternalSecure), permissionCheck)); err != nil {
return err
return nil, err
}
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
return err
return nil, err
}
if err := apis.RegisterService(ctx, settings.CreateServer(commands, queries, config.ExternalSecure)); err != nil {
return err
return nil, err
}
if err := apis.RegisterService(ctx, org.CreateServer(commands, queries, permissionCheck)); err != nil {
return err
return nil, err
}
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries)); err != nil {
return err
if err := apis.RegisterService(ctx, execution_v3_alpha.CreateServer(commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
@@ -412,38 +415,38 @@ func startAPIs(
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS)
if err != nil {
return err
return nil, err
}
// robots.txt handler
robotsTxtHandler, err := robots_txt.Start()
if err != nil {
return fmt.Errorf("unable to start robots txt handler: %w", err)
return nil, fmt.Errorf("unable to start robots txt handler: %w", err)
}
apis.RegisterHandlerOnPrefix(robots_txt.HandlerPrefix, robotsTxtHandler)
// TODO: Record openapi access logs?
openAPIHandler, err := openapi.Start()
if err != nil {
return fmt.Errorf("unable to start openapi handler: %w", err)
return nil, fmt.Errorf("unable to start openapi handler: %w", err)
}
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler)
oidcServer, err := oidc.NewServer(config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog())
if err != nil {
return fmt.Errorf("unable to start oidc provider: %w", err)
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
}
apis.RegisterHandlerPrefixes(oidcServer, oidcPrefixes...)
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor)
if err != nil {
return fmt.Errorf("unable to start saml provider: %w", err)
return nil, fmt.Errorf("unable to start saml provider: %w", err)
}
apis.RegisterHandlerOnPrefix(saml.HandlerPrefix, samlProvider.HttpHandler())
c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal)
if err != nil {
return fmt.Errorf("unable to start console: %w", err)
return nil, fmt.Errorf("unable to start console: %w", err)
}
apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c)
consolePath := console.HandlerPrefix + "/"
@@ -469,18 +472,18 @@ func startAPIs(
feature.NewCheck(eventstore),
)
if err != nil {
return fmt.Errorf("unable to start login: %w", err)
return nil, fmt.Errorf("unable to start login: %w", err)
}
apis.RegisterHandlerOnPrefix(login.HandlerPrefix, l.Handler())
apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix)
// After OIDC provider so that the callback endpoint can be used
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcServer, config.ExternalSecure)); err != nil {
return err
return nil, err
}
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
apis.RouteGRPC()
return nil
return apis, nil
}
func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls.Config, shutdown <-chan os.Signal) error {
@@ -551,3 +554,9 @@ func showBasicInformation(startConfig *Config) {
}
fmt.Printf("\n ===============================================================\n\n")
}
func checkExisting(values []string) func(string) bool {
return func(value string) bool {
return slices.Contains(values, value)
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"net/http"
"sort"
"strings"
"github.com/gorilla/mux"
@@ -38,6 +39,30 @@ type API struct {
queries *query.Queries
}
func (a *API) ListGrpcServices() []string {
serviceInfo := a.grpcServer.GetServiceInfo()
services := make([]string, len(serviceInfo))
i := 0
for servicename := range serviceInfo {
services[i] = servicename
i++
}
sort.Strings(services)
return services
}
func (a *API) ListGrpcMethods() []string {
serviceInfo := a.grpcServer.GetServiceInfo()
methods := make([]string, 0)
for servicename, service := range serviceInfo {
for _, method := range service.Methods {
methods = append(methods, "/"+servicename+"/"+method.Name)
}
}
sort.Strings(methods)
return methods
}
type healthCheck interface {
Health(ctx context.Context) error
}

View File

@@ -0,0 +1,124 @@
package execution
import (
"context"
"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"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
)
func (s *Server) ListExecutionFunctions(_ context.Context, _ *execution.ListExecutionFunctionsRequest) (*execution.ListExecutionFunctionsResponse, error) {
return &execution.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}, nil
}
func (s *Server) ListExecutionMethods(_ context.Context, _ *execution.ListExecutionMethodsRequest) (*execution.ListExecutionMethodsResponse, error) {
return &execution.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}, nil
}
func (s *Server) ListExecutionServices(_ context.Context, _ *execution.ListExecutionServicesRequest) (*execution.ListExecutionServicesResponse, error) {
return &execution.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}, nil
}
func (s *Server) SetExecution(ctx context.Context, req *execution.SetExecutionRequest) (*execution.SetExecutionResponse, error) {
set := &command.SetExecution{
Targets: req.GetTargets(),
Includes: req.GetIncludes(),
}
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *execution.SetConditions_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
details, err = s.command.SetExecutionRequest(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *execution.SetConditions_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
details, err = s.command.SetExecutionResponse(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *execution.SetConditions_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
details, err = s.command.SetExecutionEvent(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *execution.SetConditions_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
}
return &execution.SetExecutionResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteExecution(ctx context.Context, req *execution.DeleteExecutionRequest) (*execution.DeleteExecutionResponse, error) {
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *execution.SetConditions_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
details, err = s.command.DeleteExecutionRequest(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *execution.SetConditions_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
details, err = s.command.DeleteExecutionResponse(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *execution.SetConditions_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
details, err = s.command.DeleteExecutionEvent(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *execution.SetConditions_Function:
details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
}
return &execution.DeleteExecutionResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,11 @@ var _ execution.ExecutionServiceServer = (*Server)(nil)
type Server struct {
execution.UnimplementedExecutionServiceServer
command *command.Commands
query *query.Queries
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
ListGRPCMethods func() []string
ListGRPCServices func() []string
}
type Config struct{}
@@ -23,10 +26,16 @@ type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
listActionFunctions func() []string,
listGRPCMethods func() []string,
listGRPCServices func() []string,
) *Server {
return &Server{
command: command,
query: query,
command: command,
query: query,
ListActionFunctions: listActionFunctions,
ListGRPCMethods: listGRPCMethods,
ListGRPCServices: listGRPCServices,
}
}

View File

@@ -0,0 +1,33 @@
//go:build integration
package execution_test
import (
"context"
"os"
"testing"
"time"
"github.com/zitadel/zitadel/internal/integration"
execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha"
)
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()
}())
}

View File

@@ -5,7 +5,6 @@ package execution_test
import (
"context"
"fmt"
"os"
"testing"
"time"
@@ -20,26 +19,6 @@ import (
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

View File

@@ -0,0 +1,285 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ExecutionAPICondition struct {
Method string
Service string
All bool
}
func (e *ExecutionAPICondition) IsValid() error {
if e.Method == "" && e.Service == "" && !e.All {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-3tkej630e6", "Errors.Execution.Invalid")
}
// never set two conditions
if e.Method != "" && (e.Service != "" || e.All) ||
e.Service != "" && (e.Method != "" || e.All) ||
e.All && (e.Method != "" || e.Service != "") {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-nee5q8aszq", "Errors.Execution.Invalid")
}
return nil
}
func (e *ExecutionAPICondition) ID(executionType domain.ExecutionType) string {
if e.Method != "" {
return execution.ID(executionType, e.Method)
}
if e.Service != "" {
return execution.ID(executionType, e.Service)
}
if e.All {
return execution.IDAll(executionType)
}
return ""
}
func (e *ExecutionAPICondition) Existing(c *Commands) error {
if e.Method != "" && !c.GrpcMethodExisting(e.Method) {
return zerrors.ThrowNotFound(nil, "COMMAND-vysplsevt8", "Errors.Execution.ConditionInvalid")
}
if e.Service != "" && !c.GrpcServiceExisting(e.Service) {
return zerrors.ThrowNotFound(nil, "COMMAND-qu6dfhiioq", "Errors.Execution.ConditionInvalid")
}
return nil
}
func (c *Commands) SetExecutionRequest(ctx context.Context, cond *ExecutionAPICondition, set *SetExecution, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
if err := cond.Existing(c); err != nil {
return nil, err
}
if set.AggregateID == "" {
set.AggregateID = cond.ID(domain.ExecutionTypeRequest)
}
return c.setExecution(ctx, set, resourceOwner)
}
func (c *Commands) SetExecutionResponse(ctx context.Context, cond *ExecutionAPICondition, set *SetExecution, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
if err := cond.Existing(c); err != nil {
return nil, err
}
if set.AggregateID == "" {
set.AggregateID = cond.ID(domain.ExecutionTypeResponse)
}
return c.setExecution(ctx, set, resourceOwner)
}
type ExecutionFunctionCondition string
func (e ExecutionFunctionCondition) IsValid() error {
if e == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-5folwn5jws", "Errors.Execution.Invalid")
}
return nil
}
func (e ExecutionFunctionCondition) ID() string {
return execution.ID(domain.ExecutionTypeFunction, string(e))
}
func (e ExecutionFunctionCondition) Existing(c *Commands) error {
if !c.ActionFunctionExisting(string(e)) {
return zerrors.ThrowNotFound(nil, "COMMAND-cdy39t0ksr", "Errors.Execution.ConditionInvalid")
}
return nil
}
func (c *Commands) SetExecutionFunction(ctx context.Context, cond ExecutionFunctionCondition, set *SetExecution, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
if err := cond.Existing(c); err != nil {
return nil, err
}
if set.AggregateID == "" {
set.AggregateID = cond.ID()
}
return c.setExecution(ctx, set, resourceOwner)
}
type ExecutionEventCondition struct {
Event string
Group string
All bool
}
func (e *ExecutionEventCondition) IsValid() error {
if e.Event == "" && e.Group == "" && !e.All {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-w5smb6v7qu", "Errors.Execution.Invalid")
}
// never set two conditions
if e.Event != "" && (e.Group != "" || e.All) ||
e.Group != "" && (e.Event != "" || e.All) ||
e.All && (e.Event != "" || e.Group != "") {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-hdm4zl1hmd", "Errors.Execution.Invalid")
}
return nil
}
func (e *ExecutionEventCondition) ID() string {
if e.Event != "" {
return execution.ID(domain.ExecutionTypeEvent, e.Event)
}
if e.Group != "" {
return execution.ID(domain.ExecutionTypeEvent, e.Group)
}
if e.All {
return execution.IDAll(domain.ExecutionTypeEvent)
}
return ""
}
func (e *ExecutionEventCondition) Existing(c *Commands) error {
if e.Event != "" && !c.EventExisting(e.Event) {
return zerrors.ThrowNotFound(nil, "COMMAND-74aaqj8fv9", "Errors.Execution.ConditionInvalid")
}
if e.Group != "" && !c.EventGroupExisting(e.Group) {
return zerrors.ThrowNotFound(nil, "COMMAND-er5oneb5lz", "Errors.Execution.ConditionInvalid")
}
return nil
}
func (c *Commands) SetExecutionEvent(ctx context.Context, cond *ExecutionEventCondition, set *SetExecution, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
if err := cond.Existing(c); err != nil {
return nil, err
}
if set.AggregateID == "" {
set.AggregateID = cond.ID()
}
return c.setExecution(ctx, set, resourceOwner)
}
type SetExecution struct {
models.ObjectRoot
Targets []string
Includes []string
}
func (e *SetExecution) IsValid() error {
if len(e.Targets) == 0 && len(e.Includes) == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-56bteot2uj", "Errors.Execution.NoTargets")
}
if len(e.Targets) > 0 && len(e.Includes) > 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-5zleae34r1", "Errors.Execution.Invalid")
}
return nil
}
func (e *SetExecution) Existing(c *Commands, ctx context.Context, resourceOwner string) error {
if len(e.Targets) > 0 && !c.existsTargetsByIDs(ctx, e.Targets, resourceOwner) {
return zerrors.ThrowNotFound(nil, "COMMAND-17e8fq1ggk", "Errors.Target.NotFound")
}
if len(e.Includes) > 0 && !c.existsExecutionsByIDs(ctx, e.Includes, resourceOwner) {
return zerrors.ThrowNotFound(nil, "COMMAND-slgj0l4cdz", "Errors.Execution.IncludeNotFound")
}
return nil
}
func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if resourceOwner == "" || set.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-gg3a6ol4om", "Errors.IDMissing")
}
if err := set.IsValid(); err != nil {
return nil, err
}
wm := NewExecutionWriteModel(set.AggregateID, resourceOwner)
// Check if targets and includes for execution are existing
if err := set.Existing(c, ctx, resourceOwner); err != nil {
return nil, err
}
if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEvent(
ctx,
ExecutionAggregateFromWriteModel(&wm.WriteModel),
set.Targets,
set.Includes,
)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) DeleteExecutionRequest(ctx context.Context, cond *ExecutionAPICondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(domain.ExecutionTypeRequest), resourceOwner)
}
func (c *Commands) DeleteExecutionResponse(ctx context.Context, cond *ExecutionAPICondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(domain.ExecutionTypeResponse), resourceOwner)
}
func (c *Commands) DeleteExecutionFunction(ctx context.Context, cond ExecutionFunctionCondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(), resourceOwner)
}
func (c *Commands) DeleteExecutionEvent(ctx context.Context, cond *ExecutionEventCondition, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if err := cond.IsValid(); err != nil {
return nil, err
}
return c.deleteExecution(ctx, cond.ID(), resourceOwner)
}
func (c *Commands) deleteExecution(ctx context.Context, aggID string, resourceOwner string) (_ *domain.ObjectDetails, err error) {
if resourceOwner == "" || aggID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-cnic97c0g3", "Errors.IDMissing")
}
wm, err := c.getExecutionWriteModelByID(ctx, aggID, resourceOwner)
if err != nil {
return nil, err
}
if !wm.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-suq2upd3rt", "Errors.Execution.NotFound")
}
if err := c.pushAppendAndReduce(ctx, wm, execution.NewRemovedEvent(
ctx,
ExecutionAggregateFromWriteModel(&wm.WriteModel),
)); err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) existsExecutionsByIDs(ctx context.Context, ids []string, resourceOwner string) bool {
wm := NewExecutionsExistWriteModel(ids, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)
if err != nil {
return false
}
return wm.AllExists()
}
func (c *Commands) getExecutionWriteModelByID(ctx context.Context, id string, resourceOwner string) (*ExecutionWriteModel, error) {
wm := NewExecutionWriteModel(id, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)
if err != nil {
return nil, err
}
return wm, nil
}

View File

@@ -0,0 +1,113 @@
package command
import (
"slices"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/execution"
)
type ExecutionWriteModel struct {
eventstore.WriteModel
Targets []string
Includes []string
}
func (e *ExecutionWriteModel) Exists() bool {
return len(e.Targets) > 0 || len(e.Includes) > 0
}
func NewExecutionWriteModel(id string, resourceOwner string) *ExecutionWriteModel {
return &ExecutionWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: id,
ResourceOwner: resourceOwner,
InstanceID: resourceOwner,
},
}
}
func (wm *ExecutionWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *execution.SetEvent:
wm.Targets = e.Targets
wm.Includes = e.Includes
case *execution.RemovedEvent:
wm.Targets = nil
wm.Includes = nil
}
}
return wm.WriteModel.Reduce()
}
func (wm *ExecutionWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(execution.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(execution.SetEventType,
execution.RemovedEventType).
Builder()
}
func ExecutionAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,
Type: execution.AggregateType,
ResourceOwner: wm.ResourceOwner,
InstanceID: wm.InstanceID,
Version: execution.AggregateVersion,
}
}
type ExecutionsExistWriteModel struct {
eventstore.WriteModel
ids []string
existingIDs []string
}
func (e *ExecutionsExistWriteModel) AllExists() bool {
return len(e.ids) == len(e.existingIDs)
}
func NewExecutionsExistWriteModel(ids []string, resourceOwner string) *ExecutionsExistWriteModel {
return &ExecutionsExistWriteModel{
WriteModel: eventstore.WriteModel{
ResourceOwner: resourceOwner,
InstanceID: resourceOwner,
},
ids: ids,
}
}
func (wm *ExecutionsExistWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *execution.SetEvent:
if !slices.Contains(wm.existingIDs, e.Aggregate().ID) {
wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID)
}
case *execution.RemovedEvent:
i := slices.Index(wm.existingIDs, e.Aggregate().ID)
if i >= 0 {
wm.existingIDs = slices.Delete(wm.existingIDs, i, i+1)
}
}
}
return wm.WriteModel.Reduce()
}
func (wm *ExecutionsExistWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(execution.AggregateType).
AggregateIDs(wm.ids...).
EventTypes(execution.SetEventType,
execution.RemovedEventType).
Builder()
}

View File

@@ -0,0 +1,437 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/execution"
)
func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
ids []string
resourceOwner string
}
tests := []struct {
name string
fields fields
args args
res bool
}{
{
name: "execution, single",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution"},
},
res: true,
},
{
name: "execution, single reset",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution"},
},
res: true,
},
{
name: "execution, single before removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution"},
},
res: true,
},
{
name: "execution, single removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution", "org1"),
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution"},
},
res: false,
},
{
name: "execution, multiple",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: true,
},
{
name: "execution, multiple, first removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: false,
},
{
name: "execution, multiple, second removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: false,
},
{
name: "execution, multiple, third removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: false,
},
{
name: "execution, multiple, before removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: true,
},
{
name: "execution, multiple, all removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: false,
},
{
name: "execution, multiple, two removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution1", "org1"),
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
),
),
eventFromEventPusher(
execution.NewRemovedEvent(context.Background(),
execution.NewAggregate("execution2", "org1"),
),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"execution1", "execution2", "execution3"},
},
res: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
assert.Equal(t, tt.res, c.existsExecutionsByIDs(tt.args.ctx, tt.args.ids, tt.args.resourceOwner))
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -52,8 +52,14 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
return nil, err
}
}
wm, err := c.getTargetWriteModelByID(ctx, add.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
if wm.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists")
}
wm := NewTargetWriteModel(add.AggregateID, resourceOwner)
pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent(
ctx,
TargetAggregateFromWriteModel(&wm.WriteModel),
@@ -167,6 +173,15 @@ func (c *Commands) DeleteTarget(ctx context.Context, id, resourceOwner string) (
return writeModelToObjectDetails(&existing.WriteModel), nil
}
func (c *Commands) existsTargetsByIDs(ctx context.Context, ids []string, resourceOwner string) bool {
wm := NewTargetsExistsWriteModel(ids, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)
if err != nil {
return false
}
return wm.AllExists()
}
func (c *Commands) getTargetWriteModelByID(ctx context.Context, id string, resourceOwner string) (*TargetWriteModel, error) {
wm := NewTargetWriteModel(id, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)

View File

@@ -2,11 +2,11 @@ package command
import (
"context"
"slices"
"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"
)
@@ -62,7 +62,7 @@ func (wm *TargetWriteModel) Reduce() error {
if e.InterruptOnError != nil {
wm.InterruptOnError = *e.InterruptOnError
}
case *action.RemovedEvent:
case *target.RemovedEvent:
wm.State = domain.TargetRemoved
}
}
@@ -116,6 +116,54 @@ func (wm *TargetWriteModel) NewChangedEvent(
return target.NewChangedEvent(ctx, agg, changes)
}
type TargetsExistsWriteModel struct {
eventstore.WriteModel
ids []string
existingIDs []string
}
func (e *TargetsExistsWriteModel) AllExists() bool {
return len(e.ids) == len(e.existingIDs)
}
func NewTargetsExistsWriteModel(ids []string, resourceOwner string) *TargetsExistsWriteModel {
return &TargetsExistsWriteModel{
WriteModel: eventstore.WriteModel{
ResourceOwner: resourceOwner,
InstanceID: resourceOwner,
},
ids: ids,
}
}
func (wm *TargetsExistsWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *target.AddedEvent:
if !slices.Contains(wm.existingIDs, e.Aggregate().ID) {
wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID)
}
case *target.RemovedEvent:
i := slices.Index(wm.existingIDs, e.Aggregate().ID)
if i >= 0 {
wm.existingIDs = slices.Delete(wm.existingIDs, i, i+1)
}
}
}
return wm.WriteModel.Reduce()
}
func (wm *TargetsExistsWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(target.AggregateType).
AggregateIDs(wm.ids...).
EventTypes(target.AddedEventType,
target.RemovedEventType).
Builder()
}
func TargetAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: wm.AggregateID,

View File

@@ -0,0 +1,330 @@
package command
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/target"
)
func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
return target.NewAddedEvent(context.Background(),
target.NewAggregate(aggID, resourceOwner),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
)
}
func targetRemoveEvent(aggID, resourceOwner string) *target.RemovedEvent {
return target.NewRemovedEvent(context.Background(),
target.NewAggregate(aggID, resourceOwner),
"name",
)
}
func TestCommandSide_targetsExistsWriteModel(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
ids []string
resourceOwner string
}
tests := []struct {
name string
fields fields
args args
res bool
}{
{
name: "target, single",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target"},
},
res: true,
},
{
name: "target, single reset",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target", "org1"),
),
eventFromEventPusher(
targetAddEvent("target", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target"},
},
res: true,
},
{
name: "target, single before removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetRemoveEvent("target", "org1"),
),
eventFromEventPusher(
targetAddEvent("target", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target"},
},
res: true,
},
{
name: "target, single removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target"},
},
res: false,
},
{
name: "target, multiple",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: true,
},
{
name: "target, multiple, first removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: false,
},
{
name: "target, multiple, second removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: false,
},
{
name: "target, multiple, third removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target3", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: false,
},
{
name: "target, multiple, before removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetRemoveEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: true,
},
{
name: "target, multiple, all removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target2", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target3", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: false,
},
{
name: "target, multiple, two removed",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
targetAddEvent("target1", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target1", "org1"),
),
eventFromEventPusher(
targetAddEvent("target2", "org1"),
),
eventFromEventPusher(
targetAddEvent("target3", "org1"),
),
eventFromEventPusher(
targetRemoveEvent("target2", "org1"),
),
),
),
},
args: args{
ctx: context.Background(),
ids: []string{"target1", "target2", "target3"},
},
res: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
assert.Equal(t, tt.res, c.existsTargetsByIDs(tt.args.ctx, tt.args.ids, tt.args.resourceOwner))
})
}
}

View File

@@ -122,6 +122,7 @@ func TestCommands_AddTarget(t *testing.T) {
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewAddedEvent(context.Background(),
@@ -151,10 +152,43 @@ func TestCommands_AddTarget(t *testing.T) {
err: zerrors.IsPreconditionFailed,
},
},
{
"already existing",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
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{
err: zerrors.IsErrorAlreadyExists,
},
},
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
@@ -190,6 +224,7 @@ func TestCommands_AddTarget(t *testing.T) {
"push full ok",
fields{
eventstore: eventstoreExpect(t,
expectFilter(),
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),

View File

@@ -74,6 +74,12 @@ type Commands struct {
defaultSecretGenerators *SecretGenerators
samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error)
GrpcMethodExisting func(method string) bool
GrpcServiceExisting func(method string) bool
ActionFunctionExisting func(function string) bool
EventExisting func(event string) bool
EventGroupExisting func(group string) bool
}
func StartCommands(
@@ -132,6 +138,13 @@ func StartCommands(
defaultRefreshTokenIdleLifetime: defaultRefreshTokenIdleLifetime,
defaultSecretGenerators: defaultSecretGenerators,
samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.Size),
// always true for now until we can check with an eventlist
EventExisting: func(event string) bool { return true },
// always true for now until we can check with an eventlist
EventGroupExisting: func(group string) bool { return true },
GrpcServiceExisting: func(service string) bool { return false },
GrpcMethodExisting: func(method string) bool { return false },
ActionFunctionExisting: domain.FunctionExists(),
}
repo.codeAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)

View File

@@ -0,0 +1,33 @@
package domain
type ExecutionType uint
func (s ExecutionType) Valid() bool {
return s < executionTypeStateCount
}
const (
ExecutionTypeUnspecified ExecutionType = iota
ExecutionTypeRequest
ExecutionTypeResponse
ExecutionTypeFunction
ExecutionTypeEvent
executionTypeStateCount
)
func (e ExecutionType) String() string {
switch e {
case ExecutionTypeUnspecified, executionTypeStateCount:
return ""
case ExecutionTypeRequest:
return "request"
case ExecutionTypeResponse:
return "response"
case ExecutionTypeFunction:
return "function"
case ExecutionTypeEvent:
return "event"
}
return ""
}

View File

@@ -1,6 +1,9 @@
package domain
import "strconv"
import (
"slices"
"strconv"
)
type FlowState int32
@@ -25,6 +28,15 @@ const (
flowTypeCount
)
func AllFlowTypes() []FlowType {
return []FlowType{
FlowTypeExternalAuthentication,
FlowTypeCustomiseToken,
FlowTypeInternalAuthentication,
FlowTypeCustomizeSAMLResponse,
}
}
func (s FlowType) Valid() bool {
return s > 0 && s < flowTypeCount
}
@@ -138,3 +150,20 @@ func (s TriggerType) LocalizationKey() string {
return "Action.TriggerType.Unspecified"
}
}
func AllFunctions() []string {
functions := make([]string, 0)
for _, flowType := range AllFlowTypes() {
for _, triggerType := range flowType.TriggerTypes() {
functions = append(functions, flowType.LocalizationKey()+"."+triggerType.LocalizationKey())
}
}
return functions
}
func FunctionExists() func(string) bool {
functions := AllFunctions()
return func(s string) bool {
return slices.Contains(functions, s)
}
}

View File

@@ -525,3 +525,12 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *execution.Crea
require.NoError(t, err)
return target
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *execution.SetConditions, targets []string) *execution.SetExecutionResponse {
target, err := s.Client.ExecutionV3.SetExecution(ctx, &execution.SetExecutionRequest{
Condition: cond,
Targets: targets,
})
require.NoError(t, err)
return target
}

View File

@@ -0,0 +1,31 @@
package execution
import (
"strings"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
AggregateType = "execution"
AggregateVersion = "v1"
)
func NewAggregate(aggrID, instanceID string) *eventstore.Aggregate {
return &eventstore.Aggregate{
ID: aggrID,
Type: AggregateType,
ResourceOwner: instanceID,
InstanceID: instanceID,
Version: AggregateVersion,
}
}
func ID(executionType domain.ExecutionType, value string) string {
return strings.Join([]string{executionType.String(), value}, ".")
}
func IDAll(executionType domain.ExecutionType) string {
return executionType.String()
}

View File

@@ -0,0 +1,8 @@
package execution
import "github.com/zitadel/zitadel/internal/eventstore"
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SetEventType, eventstore.GenericEventMapper[SetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent])
}

View File

@@ -0,0 +1,71 @@
package execution
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
eventTypePrefix eventstore.EventType = "execution."
SetEventType = eventTypePrefix + "set"
RemovedEventType = eventTypePrefix + "removed"
)
type SetEvent struct {
*eventstore.BaseEvent `json:"-"`
ExecutionType domain.ExecutionType `json:"executionType"`
Targets []string `json:"targets"`
Includes []string `json:"includes"`
}
func (e *SetEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *SetEvent) Payload() any {
return e
}
func (e *SetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
targets []string,
includes []string,
) *SetEvent {
return &SetEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx, aggregate, SetEventType,
),
Targets: targets,
Includes: includes,
}
}
type RemovedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *RemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *RemovedEvent) Payload() any {
return e
}
func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *RemovedEvent {
return &RemovedEvent{
eventstore.NewBaseEventForPush(ctx, aggregate, RemovedEventType),
}
}

View File

@@ -557,6 +557,12 @@ Errors:
NoTimeout: Целта няма време за изчакване
InvalidURL: Целта има невалиден URL адрес
NotFound: Целта не е намерена
Execution:
ConditionInvalid: Условието за изпълнение е невалидно
Invalid: Изпълнението е невалидно
NotFound: Изпълнението не е намерено
IncludeNotFound: Включването не е намерено
NoTargets: Няма определени цели
AggregateTypes:
action: Действие
@@ -569,8 +575,12 @@ AggregateTypes:
quota: Квота
feature: Особеност
target: Целта
execution: Екзекуция
EventTypes:
execution:
set: Комплект за изпълнение
removed: Изпълнението е изтрито
target:
added: Целта е създадена
changed: Целта е променена

View File

@@ -537,6 +537,12 @@ Errors:
NoTimeout: Cíl nemá časový limit
InvalidURL: Cíl má neplatnou adresu URL
NotFound: Cíl nenalezen
Execution:
ConditionInvalid: Podmínka provedení je neplatná
Invalid: Provedení je neplatné
NotFound: Provedení nenalezeno
IncludeNotFound: Zahrnout nenalezeno
NoTargets: Nejsou definovány žádné cíle
AggregateTypes:
action: Akce
@@ -549,8 +555,12 @@ AggregateTypes:
quota: Kvóta
feature: Funkce
target: Cíl
execution: Provedení
EventTypes:
execution:
set: Prováděcí sada
removed: Provedení smazáno
target:
added: Cíl vytvořen
changed: Cíl změněn

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: Ziel hat keinen Timeout
InvalidURL: Ziel hat eine ungültige URL
NotFound: Ziel nicht gefunden
Execution:
ConditionInvalid: Die Ausführungsbedingung ist ungültig
Invalid: Die Ausführung ist ungültig
NotFound: Ausführung nicht gefunden
IncludeNotFound: Einschließen nicht gefunden
NoTargets: Keine Ziele definiert
AggregateTypes:
action: Action
@@ -552,8 +558,12 @@ AggregateTypes:
quota: Kontingent
feature: Feature
target: Ziel
execution: Ausführung
EventTypes:
execution:
set: Ausführung gesetzt
removed: Ausführung gelöscht
target:
added: Ziel erstellt
changed: Ziel geändert

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: Target has no timeout
InvalidURL: Target has an invalid URL
NotFound: Target not found
Execution:
ConditionInvalid: Execution condition is invalid
Invalid: Execution is invalid
NotFound: Execution not found
IncludeNotFound: Include not found
NoTargets: No targets defined
AggregateTypes:
action: Action
@@ -552,8 +558,12 @@ AggregateTypes:
quota: Quota
feature: Feature
target: Target
execution: Execution
EventTypes:
execution:
set: Execution set
removed: Execution deleted
target:
added: Target created
changed: Target changed

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: El objetivo no tiene tiempo de espera
InvalidURL: El objetivo tiene una URL no válida
NotFound: El objetivo no encontrado
Execution:
ConditionInvalid: La condición de ejecución no es válida
Invalid: La ejecución no es válida
NotFound: Ejecución no encontrada
IncludeNotFound: Incluir no encontrado
NoTargets: No hay objetivos definidos
AggregateTypes:
action: Acción
@@ -552,8 +558,12 @@ AggregateTypes:
quota: Cuota
feature: Característica
target: Objectivo
execution: Ejecución
EventTypes:
execution:
set: Conjunto de ejecución
removed: Ejecución eliminada
target:
added: Objetivo creado
changed: Objetivo cambiado

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: La cible n'a pas de délai d'attente
InvalidURL: La cible a une URL non valide
NotFound: La cible introuvable
Execution:
ConditionInvalid: La condition d'exécution n'est pas valide
Invalid: L'exécution est invalide
NotFound: Exécution introuvable
IncludeNotFound: Inclure introuvable
NoTargets: Aucune cible définie
AggregateTypes:
action: Action
@@ -552,8 +558,12 @@ AggregateTypes:
quota: Contingent
feature: Fonctionnalité
target: Cible
execution: Exécution
EventTypes:
execution:
set: Ensemble d'exécution
removed: Exécution supprimée
target:
added: Cible créée
changed: Cible modifiée

View File

@@ -541,6 +541,13 @@ Errors:
NoTimeout: Il target non ha timeout
InvalidURL: La destinazione ha un URL non valido
NotFound: Obiettivo non trovato
Execution:
ConditionInvalid: La condizione di esecuzione non è valida
Invalid: L'esecuzione non è valida
NotFound: Esecuzione non trovata
IncludeNotFound: Includi non trovato
NoTargets: Nessun obiettivo definito
AggregateTypes:
action: Azione
@@ -553,8 +560,12 @@ AggregateTypes:
quota: Quota
feature: Funzionalità
target: Bersaglio
execution: Esecuzione
EventTypes:
execution:
set: Insieme di esecuzione
removed: Esecuzione cancellata
target:
added: Obiettivo creato
changed: Obiettivo cambiato

View File

@@ -529,6 +529,12 @@ Errors:
NoTimeout: ターゲットにはタイムアウトがありません
InvalidURL: ターゲットに無効な URL があります
NotFound: ターゲットが見つかりません
Execution:
ConditionInvalid: 実行条件が不正です
Invalid: 実行は無効です
NotFound: 実行が見つかりませんでした
IncludeNotFound: 見つからないものを含める
NoTargets: ターゲットが定義されていません
AggregateTypes:
action: アクション
@@ -541,8 +547,12 @@ AggregateTypes:
quota: クォータ
feature: 特徴
target: 目標
execution: 実行
EventTypes:
execution:
set: 実行セット
removed: 実行は削除されました
target:
added: ターゲットが作成されました
changed: ターゲットが変更されました

View File

@@ -539,6 +539,12 @@ Errors:
NoTimeout: Целта нема тајмаут
InvalidURL: Целта има неважечка URL-адреса
NotFound: Целта не е пронајдена
Execution:
ConditionInvalid: Условот за извршување е неважечки
Invalid: Извршувањето е неважечко
NotFound: Извршувањето не е пронајдено
IncludeNotFound: Вклучете не е пронајден
NoTargets: Не се дефинирани цели
AggregateTypes:
action: Акција
@@ -551,8 +557,12 @@ AggregateTypes:
quota: Квота
feature: Карактеристика
target: Цел
execution: Извршување
EventTypes:
execution:
set: Комплет за извршување
removed: Извршувањето е избришано
target:
added: Целта е избришана
changed: Целта е променета

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: Doel heeft geen time-out
InvalidURL: Doel heeft een ongeldige URL
NotFound: Doel niet gevonden
Execution:
ConditionInvalid: Uitvoeringsvoorwaarde is ongeldig
Invalid: Uitvoering is ongeldig
NotFound: Uitvoering niet gevonden
IncludeNotFound: Inclusief niet gevonden
NoTargets: Geen doelstellingen gedefinieerd
AggregateTypes:
action: Actie
@@ -552,8 +558,12 @@ AggregateTypes:
quota: Quota
feature: Functie
target: Doel
execution: Executie
EventTypes:
execution:
set: Uitvoering ingesteld
removed: Uitvoering verwijderd
target:
added: Doel gemaakt
changed: Doel gewijzigd

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: Cel nie ma limitu czasu
InvalidURL: Cel ma nieprawidłowy adres URL
NotFound: Nie znaleziono celu
Execution:
ConditionInvalid: Warunek wykonania jest nieprawidłowy
Invalid: Wykonanie jest nieprawidłowe
NotFound: Nie znaleziono wykonania
IncludeNotFound: Nie znaleziono uwzględnienia
NoTargets: Nie zdefiniowano celów
AggregateTypes:
action: Działanie
@@ -552,8 +558,12 @@ AggregateTypes:
quota: Limit
feature: Funkcja
target: Cel
execution: Wykonanie
EventTypes:
execution:
set: Zestaw wykonawczy
removed: Wykonanie usunięte
target:
added: Cel został utworzony
changed: Cel zmieniony

View File

@@ -534,6 +534,12 @@ Errors:
NoTimeout: O destino não tem tempo limite
InvalidURL: O destino tem um URL inválido
NotFound: Destino não encontrado
Execution:
ConditionInvalid: A condição de execução é inválida
Invalid: A execução é inválida
NotFound: Execução não encontrada
IncludeNotFound: Incluir não encontrado
NoTargets: Nenhuma meta definida
AggregateTypes:
action: Ação
@@ -545,9 +551,13 @@ AggregateTypes:
usergrant: Concessão de usuário
quota: Cota
feature: Recurso
target: objetivo
target: Objetivo
execution: Execução
EventTypes:
execution:
set: Conjunto de execução
removed: Execução excluída
target:
added: Destino criado
changed: Destino alterada

View File

@@ -528,6 +528,12 @@ Errors:
NoTimeout: У цели нет тайм-аута
InvalidURL: Цель имеет неверный URL-адрес
NotFound: Цель не найдена
Execution:
ConditionInvalid: Недопустимое условие выполнения
Invalid: Исполнение недействительно
NotFound: Исполнение не найдено
IncludeNotFound: Включить не найдено
NoTargets: Цели не определены
AggregateTypes:
action: Действие
@@ -540,8 +546,12 @@ AggregateTypes:
quota: Квота
feature: Особенность
target: мишень
execution: Исполнение
EventTypes:
execution:
set: Набор исполнения
removed: Исполнение удалено
target:
added: Цель создана
changed: Цель изменена

View File

@@ -540,6 +540,12 @@ Errors:
NoTimeout: 目标没有超时
InvalidURL: 目标的 URL 无效
NotFound: 未找到目标
Execution:
ConditionInvalid: 执行条件无效
Invalid: 执行无效
NotFound: 未找到执行
IncludeNotFound: 包括未找到的内容
NoTargets: 没有定义目标
AggregateTypes:
action: 动作
@@ -552,8 +558,12 @@ AggregateTypes:
quota: 配额
feature: 特征
target:
execution: 执行
EventTypes:
execution:
set: 执行集
removed: 执行已删除
target:
added: 目标已创建
changed: 目标改变

View File

@@ -0,0 +1,109 @@
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 SetConditions{
// 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.
SetRequestExecution request = 1;
// Condition-type to execute on response if a request on the defined API point happens.
SetResponseExecution response = 2;
// Condition-type to execute if function is used, replaces actions v1.
string function = 3;
// Condition-type to execute if an event is created in the system.
SetEventExecution event = 4;
}
}
message SetRequestExecution{
// Condition for the request execution, only one possible.
oneof condition{
// 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.v2beta.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.v2beta.SessionService\"";
}
];
// All calls to any available service and endpoint as condition.
bool all = 3;
}
}
message SetResponseExecution{
// Condition for the response execution, only one possible.
oneof condition{
// 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.v2beta.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.v2beta.SessionService\"";
}
];
// All calls to any available service and endpoint as condition.
bool all = 3;
}
}
message SetEventExecution{
// Condition for the event execution, only one possible.
oneof condition{
// 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;
}
}

View File

@@ -9,6 +9,7 @@ import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/execution/v3alpha/target.proto";
import "zitadel/execution/v3alpha/execution.proto";
import "zitadel/object/v2beta/object.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
@@ -185,6 +186,125 @@ service ExecutionService {
};
};
}
// Set an execution
//
// Set an execution to call a previously defined target or include the targets of a previously defined execution.
rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) {
option (google.api.http) = {
post: "/v3alpha/executions"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.write"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Execution successfully set";
};
};
};
}
// Delete an execution
//
// Delete an existing execution.
rpc DeleteExecution (DeleteExecutionRequest) returns (DeleteExecutionResponse) {
option (google.api.http) = {
delete: "/v3alpha/executions"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.delete"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "Execution successfully deleted";
};
};
};
}
// List all available functions
//
// List all available functions which can be used as condition for executions.
rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) {
option (google.api.http) = {
get: "/v3alpha/executions/functions"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all functions successfully";
};
};
};
}
// List all available methods
//
// List all available methods which can be used as condition for executions.
rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) {
option (google.api.http) = {
get: "/v3alpha/executions/methods"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all methods successfully";
};
};
};
}
// List all available service
//
// List all available services which can be used as condition for executions.
rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) {
option (google.api.http) = {
get: "/v3alpha/executions/services"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "execution.read"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "List all services successfully";
};
};
};
}
}
message CreateTargetRequest {
@@ -291,3 +411,44 @@ message DeleteTargetResponse {
// Details provide some base information (such as the last change date) of the target.
zitadel.object.v2beta.Details details = 1;
}
message SetExecutionRequest {
// Defines the condition type and content of the condition for execution.
SetConditions condition = 1;
// Defines the execution targets which are defined as a different resource, which are called in the defined conditions.
repeated string targets = 2;
// Defines other executions as included with the same condition-types.
repeated string includes = 3;
}
message SetExecutionResponse {
// Details provide some base information (such as the last change date) of the execution.
zitadel.object.v2beta.Details details = 2;
}
message DeleteExecutionRequest {
// Unique identifier of the execution.
SetConditions condition = 1;
}
message DeleteExecutionResponse {
// Details provide some base information (such as the last change date) of the execution.
zitadel.object.v2beta.Details details = 1;
}
message ListExecutionFunctionsRequest{}
message ListExecutionFunctionsResponse{
// All available methods
repeated string functions = 1;
}
message ListExecutionMethodsRequest{}
message ListExecutionMethodsResponse{
// All available methods
repeated string methods = 1;
}
message ListExecutionServicesRequest{}
message ListExecutionServicesResponse{
// All available methods
repeated string services = 1;
}