diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index fa940bf564..b0564d0b2c 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -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" diff --git a/cmd/start/start.go b/cmd/start/start.go index 163d1de2f2..0dc91d52f4 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -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) + } +} diff --git a/internal/api/api.go b/internal/api/api.go index 538d22f48d..c4ae07f3f3 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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 } diff --git a/internal/api/grpc/execution/v3alpha/execution.go b/internal/api/grpc/execution/v3alpha/execution.go new file mode 100644 index 0000000000..d455670130 --- /dev/null +++ b/internal/api/grpc/execution/v3alpha/execution.go @@ -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 +} diff --git a/internal/api/grpc/execution/v3alpha/execution_integration_test.go b/internal/api/grpc/execution/v3alpha/execution_integration_test.go new file mode 100644 index 0000000000..01b8b141d9 --- /dev/null +++ b/internal/api/grpc/execution/v3alpha/execution_integration_test.go @@ -0,0 +1,1280 @@ +//go:build integration + +package execution_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "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" +) + +func TestServer_SetExecution_Request(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + req *execution.SetExecutionRequest + want *execution.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "service, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_SetExecution_Request_Include(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + executionCond := "request" + Tester.SetExecution(CTX, t, + &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_All{ + All: true, + }, + }, + }, + }, + []string{targetResp.GetId()}, + ) + + tests := []struct { + name string + ctx context.Context + req *execution.SetExecutionRequest + want *execution.SetExecutionResponse + wantErr bool + }{ + { + name: "method, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Includes: []string{executionCond}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "service, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Includes: []string{executionCond}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_All{ + All: true, + }, + }, + }, + }, + Includes: []string{executionCond}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_DeleteExecution_Request(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, request *execution.DeleteExecutionRequest) error + req *execution.DeleteExecutionRequest + want *execution.DeleteExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{}, + }, + }, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/NotExisting", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/GetSession", + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "service, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_Service{ + Service: "zitadel.user.v2beta.UserService", + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Request{ + Request: &execution.SetRequestExecution{ + Condition: &execution.SetRequestExecution_All{ + All: true, + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.ctx, tt.req) + require.NoError(t, err) + } + + got, err := Client.DeleteExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_SetExecution_Response(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + req *execution.SetExecutionRequest + want *execution.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Method{ + Method: "/zitadel.session.v2beta.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "service, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Service{ + Service: "zitadel.session.v2beta.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_DeleteExecution_Response(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, request *execution.DeleteExecutionRequest) error + req *execution.DeleteExecutionRequest + want *execution.DeleteExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{}, + }, + }, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/NotExisting", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/GetSession", + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "service, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_Service{ + Service: "zitadel.user.v2beta.UserService", + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_All{ + All: true, + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.ctx, tt.req) + require.NoError(t, err) + } + + got, err := Client.DeleteExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_SetExecution_Event(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + req *execution.SetExecutionRequest + want *execution.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + /* + //TODO event existing check + + { + name: "event, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Event{ + Event: "xxx", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + */ + { + name: "event, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Event{ + Event: "xxx", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + /* + // TODO: + + { + name: "group, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Group{ + Group: "xxx", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + */ + { + name: "group, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Group{ + Group: "xxx", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_DeleteExecution_Event(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, request *execution.DeleteExecutionRequest) error + req *execution.DeleteExecutionRequest + want *execution.DeleteExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{}, + }, + }, + }, + wantErr: true, + }, + /* + //TODO: add when check is implemented + { + name: "event, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Event{ + Event: "xxx", + }, + }, + }, + }, + }, + wantErr: true, + }, + */ + { + name: "event, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Event{ + Event: "xxx", + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "group, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Group{ + Group: "xxx", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "group, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_Group{ + Group: "xxx", + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_All{ + All: true, + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + { + name: "all, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Event{ + Event: &execution.SetEventExecution{ + Condition: &execution.SetEventExecution_All{ + All: true, + }, + }, + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.ctx, tt.req) + require.NoError(t, err) + } + + got, err := Client.DeleteExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_SetExecution_Function(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + req *execution.SetExecutionRequest + want *execution.SetExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, not existing", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Function{ + Function: "xxx", + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, ok", + ctx: CTX, + req: &execution.SetExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Function{ + Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication", + }, + }, + Targets: []string{targetResp.GetId()}, + }, + want: &execution.SetExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.SetExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} + +func TestServer_DeleteExecution_Function(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, request *execution.DeleteExecutionRequest) error + req *execution.DeleteExecutionRequest + want *execution.DeleteExecutionResponse + wantErr bool + }{ + { + name: "missing permission", + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{ + Condition: &execution.SetResponseExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Response{ + Response: &execution.SetResponseExecution{}, + }, + }, + }, + wantErr: true, + }, + { + name: "function, not existing", + ctx: CTX, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Function{ + Function: "xxx", + }, + }, + }, + wantErr: true, + }, + { + name: "function, ok", + ctx: CTX, + dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + return nil + }, + req: &execution.DeleteExecutionRequest{ + Condition: &execution.SetConditions{ + ConditionType: &execution.SetConditions_Function{ + Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication", + }, + }, + }, + want: &execution.DeleteExecutionResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + err := tt.dep(tt.ctx, tt.req) + require.NoError(t, err) + } + + got, err := Client.DeleteExecution(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/execution/v3alpha/server.go b/internal/api/grpc/execution/v3alpha/server.go index aa72102486..b1a1b5cb4e 100644 --- a/internal/api/grpc/execution/v3alpha/server.go +++ b/internal/api/grpc/execution/v3alpha/server.go @@ -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, } } diff --git a/internal/api/grpc/execution/v3alpha/server_integration_test.go b/internal/api/grpc/execution/v3alpha/server_integration_test.go new file mode 100644 index 0000000000..41cdf3f8d4 --- /dev/null +++ b/internal/api/grpc/execution/v3alpha/server_integration_test.go @@ -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() + }()) +} diff --git a/internal/api/grpc/execution/v3alpha/target_integration_test.go b/internal/api/grpc/execution/v3alpha/target_integration_test.go index f3f06fab18..e45476a1ea 100644 --- a/internal/api/grpc/execution/v3alpha/target_integration_test.go +++ b/internal/api/grpc/execution/v3alpha/target_integration_test.go @@ -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 diff --git a/internal/command/action_v2_execution.go b/internal/command/action_v2_execution.go new file mode 100644 index 0000000000..164830d6e5 --- /dev/null +++ b/internal/command/action_v2_execution.go @@ -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 +} diff --git a/internal/command/action_v2_execution_model.go b/internal/command/action_v2_execution_model.go new file mode 100644 index 0000000000..0dbeb3d874 --- /dev/null +++ b/internal/command/action_v2_execution_model.go @@ -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() +} diff --git a/internal/command/action_v2_execution_model_test.go b/internal/command/action_v2_execution_model_test.go new file mode 100644 index 0000000000..4c74a146e8 --- /dev/null +++ b/internal/command/action_v2_execution_model_test.go @@ -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)) + + }) + } +} diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go new file mode 100644 index 0000000000..a9fafc9203 --- /dev/null +++ b/internal/command/action_v2_execution_test.go @@ -0,0 +1,2505 @@ +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/execution" + "github.com/zitadel/zitadel/internal/repository/target" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func existsMock(exists bool) func(method string) bool { + return func(method string) bool { + return exists + } +} + +func TestCommands_SetExecutionRequest(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + grpcMethodExists func(method string) bool + grpcServiceExists func(method string) bool + } + type args struct { + ctx context.Context + cond *ExecutionAPICondition + set *SetExecution + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + set: &SetExecution{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no valid cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "notvalid", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty executionType, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty target, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "target and include, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"invalid"}, + Includes: []string{"invalid"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "valid", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "method not found, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "service not found, error", + fields{ + eventstore: eventstoreExpect(t), + grpcServiceExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, method target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.method", "org1"), + []string{"target"}, + nil, + ), + ), + ), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, service target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.service", "org1"), + []string{"target"}, + nil, + ), + ), + ), + grpcServiceExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, all target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request", "org1"), + []string{"target"}, + nil, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push not found, method include", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + set: &SetExecution{ + Includes: []string{"request.include"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, method include", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.include", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.method", "org1"), + nil, + []string{"request.include"}, + ), + ), + ), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + set: &SetExecution{ + Includes: []string{"request.include"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push not found, service include", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + grpcServiceExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + set: &SetExecution{ + Includes: []string{"request.include"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, service include", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.include", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.service", "org1"), + nil, + []string{"request.include"}, + ), + ), + ), + grpcServiceExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + set: &SetExecution{ + Includes: []string{"request.include"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push not found, all include", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Includes: []string{"request.include"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, all include", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.include", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request", "org1"), + nil, + []string{"request.include"}, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Includes: []string{"request.include"}, + }, + 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, + GrpcMethodExisting: tt.fields.grpcMethodExists, + GrpcServiceExisting: tt.fields.grpcServiceExists, + } + details, err := c.SetExecutionRequest(tt.args.ctx, tt.args.cond, tt.args.set, 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_SetExecutionResponse(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + grpcMethodExists func(method string) bool + grpcServiceExists func(method string) bool + } + type args struct { + ctx context.Context + cond *ExecutionAPICondition + set *SetExecution + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + set: &SetExecution{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no valid cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "notvalid", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty executionType, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty target, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "target and include, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"invalid"}, + Includes: []string{"invalid"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "valid", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "method not found, error", + fields{ + eventstore: eventstoreExpect(t), + grpcMethodExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "service not found, error", + fields{ + eventstore: eventstoreExpect(t), + grpcServiceExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, method target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response.method", "org1"), + []string{"target"}, + nil, + ), + ), + ), + grpcMethodExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, service target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response.service", "org1"), + []string{"target"}, + nil, + ), + ), + ), + grpcServiceExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, all target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response", "org1"), + []string{"target"}, + nil, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + 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, + GrpcMethodExisting: tt.fields.grpcMethodExists, + GrpcServiceExisting: tt.fields.grpcServiceExists, + } + details, err := c.SetExecutionResponse(tt.args.ctx, tt.args.cond, tt.args.set, 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_SetExecutionEvent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + eventExists func(string) bool + eventGroupExists func(string) bool + } + type args struct { + ctx context.Context + cond *ExecutionEventCondition + set *SetExecution + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{}, + set: &SetExecution{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{}, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no valid cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "notvalid", + "notvalid", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty executionType, error", + fields{ + eventstore: eventstoreExpect(t), + eventExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty target, error", + fields{ + eventstore: eventstoreExpect(t), + eventExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "target and include, error", + fields{ + eventstore: eventstoreExpect(t), + eventExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "notvalid", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"invalid"}, + Includes: []string{"invalid"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + eventExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "valid", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "event not found, error", + fields{ + eventstore: eventstoreExpect(t), + eventExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "event", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "group not found, error", + fields{ + eventstore: eventstoreExpect(t), + eventGroupExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "group", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, event target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event.event", "org1"), + []string{"target"}, + nil, + ), + ), + ), + eventExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "event", + "", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, group target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event.group", "org1"), + []string{"target"}, + nil, + ), + ), + ), + eventGroupExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "group", + false, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, all target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event", "org1"), + []string{"target"}, + nil, + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + set: &SetExecution{ + Targets: []string{"target"}, + }, + 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, + EventExisting: tt.fields.eventExists, + EventGroupExisting: tt.fields.eventGroupExists, + } + details, err := c.SetExecutionEvent(tt.args.ctx, tt.args.cond, tt.args.set, 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_SetExecutionFunction(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + actionFunctionExists func(string) bool + } + type args struct { + ctx context.Context + cond ExecutionFunctionCondition + set *SetExecution + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "", + set: &SetExecution{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: "", + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty executionType, error", + fields{ + eventstore: eventstoreExpect(t), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "empty target, error", + fields{ + eventstore: eventstoreExpect(t), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "target and include, error", + fields{ + eventstore: eventstoreExpect(t), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []string{"invalid"}, + Includes: []string{"invalid"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewSetEvent(context.Background(), + execution.NewAggregate("function.function", "org1"), + []string{"target"}, + nil, + ), + ), + ), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, { + "push error, function target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push error, function not existing", + fields{ + eventstore: eventstoreExpect(t), + actionFunctionExists: existsMock(false), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []string{"target"}, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, function target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + target.NewAddedEvent(context.Background(), + target.NewAggregate("target", "org1"), + "name", + domain.TargetTypeWebhook, + "https://example.com", + time.Second, + true, + true, + ), + ), + ), + expectPush( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("function.function", "org1"), + []string{"target"}, + nil, + ), + ), + ), + actionFunctionExists: existsMock(true), + }, + args{ + ctx: context.Background(), + cond: "function", + set: &SetExecution{ + Targets: []string{"target"}, + }, + 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, + ActionFunctionExisting: tt.fields.actionFunctionExists, + } + details, err := c.SetExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.set, 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_DeleteExecutionRequest(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + cond *ExecutionAPICondition + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no valid cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "notvalid", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("request.valid", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "valid", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "not found, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, method target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.method", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("request.method", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, service target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request.service", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("request.service", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, all target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("request", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("request", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + 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.DeleteExecutionRequest(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.details, details) + } + }) + } +} + +func TestCommands_DeleteExecutionResponse(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + cond *ExecutionAPICondition + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no valid cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "notvalid", + "notvalid", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("response.valid", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "valid", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "not found, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, method target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response.method", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("response.method", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "method", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, service target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response.service", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("response.service", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "service", + false, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push ok, all target", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("response", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("response", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionAPICondition{ + "", + "", + 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.DeleteExecutionResponse(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.details, details) + } + }) + } +} + +func TestCommands_DeleteExecutionEvent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + cond *ExecutionEventCondition + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{}, + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{}, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("event.valid", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "valid", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "push error, not existing", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "valid", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push error, event", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "valid", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, event", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event.valid", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("event.valid", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "valid", + "", + false, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push error, group", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "valid", + false, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, group", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event.group", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("event.group", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "group", + false, + }, + resourceOwner: "org1", + }, + res{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + "push error, all", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + true, + }, + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, all", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("event", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("event", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: &ExecutionEventCondition{ + "", + "", + 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.DeleteExecutionEvent(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.details, details) + } + }) + } +} +func TestCommands_DeleteExecutionFunction(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + cond ExecutionFunctionCondition + resourceOwner string + } + type res struct { + details *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "no resourceowner, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: "", + resourceOwner: "", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "no cond, error", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + cond: "", + resourceOwner: "org1", + }, + res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + "push failed, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("function.function", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPushFailed( + zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("function.function", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + resourceOwner: "org1", + }, + res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + "push error, not existing", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + resourceOwner: "org1", + }, + res{ + err: zerrors.IsNotFound, + }, + }, + { + "push ok, function", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + execution.NewSetEvent(context.Background(), + execution.NewAggregate("function.function", "org1"), + []string{"target"}, + nil, + ), + ), + ), + expectPush( + execution.NewRemovedEvent(context.Background(), + execution.NewAggregate("function.function", "org1"), + ), + ), + ), + }, + args{ + ctx: context.Background(), + cond: "function", + 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.DeleteExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.details, details) + } + }) + } +} diff --git a/internal/command/action_v2_target.go b/internal/command/action_v2_target.go index 3ed2f574ae..1bd2a62070 100644 --- a/internal/command/action_v2_target.go +++ b/internal/command/action_v2_target.go @@ -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) diff --git a/internal/command/action_v2_target_model.go b/internal/command/action_v2_target_model.go index f480f28528..60f70c40e0 100644 --- a/internal/command/action_v2_target_model.go +++ b/internal/command/action_v2_target_model.go @@ -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, diff --git a/internal/command/action_v2_target_model_test.go b/internal/command/action_v2_target_model_test.go new file mode 100644 index 0000000000..4c0f31c8e0 --- /dev/null +++ b/internal/command/action_v2_target_model_test.go @@ -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)) + + }) + } +} diff --git a/internal/command/action_v2_target_test.go b/internal/command/action_v2_target_test.go index bd49254ae1..8600ffabf1 100644 --- a/internal/command/action_v2_target_test.go +++ b/internal/command/action_v2_target_test.go @@ -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"), diff --git a/internal/command/command.go b/internal/command/command.go index ffc57bd165..95a76acb8b 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -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) diff --git a/internal/domain/execution.go b/internal/domain/execution.go new file mode 100644 index 0000000000..08f215bf08 --- /dev/null +++ b/internal/domain/execution.go @@ -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 "" +} diff --git a/internal/domain/flow.go b/internal/domain/flow.go index e8fcfbccb7..143ce6bd0b 100644 --- a/internal/domain/flow.go +++ b/internal/domain/flow.go @@ -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) + } +} diff --git a/internal/integration/client.go b/internal/integration/client.go index 7a16d35418..3c928dd2ba 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -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 +} diff --git a/internal/repository/execution/aggregate.go b/internal/repository/execution/aggregate.go new file mode 100644 index 0000000000..973bb17854 --- /dev/null +++ b/internal/repository/execution/aggregate.go @@ -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() +} diff --git a/internal/repository/execution/eventstore.go b/internal/repository/execution/eventstore.go new file mode 100644 index 0000000000..bf4e7bc79f --- /dev/null +++ b/internal/repository/execution/eventstore.go @@ -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]) +} diff --git a/internal/repository/execution/execution.go b/internal/repository/execution/execution.go new file mode 100644 index 0000000000..a03a96d8be --- /dev/null +++ b/internal/repository/execution/execution.go @@ -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), + } +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index db71abadfc..85975c4f53 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -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: Целта е променена diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index dcd0d0fcc2..dfb7f75ab2 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -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 diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 0862f710be..5876a5c677 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -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 diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index c215e22938..8411c9bb79 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -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 diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index e2039e19a4..deddad8a37 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -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 diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 2bd49cdc2f..688994eaea 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -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 diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 8c7ae6688c..81ff5e51e2 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -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 diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 2ebc9711d0..22ad9a7c60 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -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: ターゲットが変更されました diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index b55246875b..59d40a57e1 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -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: Целта е променета diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 5a36ec1610..eea39453e4 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -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 diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 4cfc62f805..ebcd9284b4 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -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 diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index b705f37867..b5cc0f6dd9 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -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 diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index e8ba05475f..60a8f35689 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -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: Цель изменена diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 482dc903e4..4d30c732c3 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -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: 目标改变 diff --git a/proto/zitadel/execution/v3alpha/execution.proto b/proto/zitadel/execution/v3alpha/execution.proto new file mode 100644 index 0000000000..8a203b17ba --- /dev/null +++ b/proto/zitadel/execution/v3alpha/execution.proto @@ -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; + } +} + diff --git a/proto/zitadel/execution/v3alpha/execution_service.proto b/proto/zitadel/execution/v3alpha/execution_service.proto index 4e5cef9925..655696008c 100644 --- a/proto/zitadel/execution/v3alpha/execution_service.proto +++ b/proto/zitadel/execution/v3alpha/execution_service.proto @@ -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; +} \ No newline at end of file