diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 6c7e1cf530..b8d7952be3 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -438,32 +438,25 @@ SystemDefaults: # Passwords previously hashed with a different algorithm # or cost are automatically re-hashed using this config, # upon password validation or update. + # Configure the Hasher config by environment variable using JSON notation: + # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER='{"Algorithm":"pbkdf2","Rounds":290000,"Hash":"sha256"}' Hasher: - Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM + # Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2" + # Depending on the algorithm, different configuration options take effect. + Algorithm: bcrypt + # Cost takes effect for the algorithms bcrypt and scrypt Cost: 14 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST - - # Other supported Hasher configs: - - # Hasher: - # Algorithm: "argon2i" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM - # Time: 3 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_TIME - # Memory: 32768 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_MEMORY - # Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS - - # Hasher: - # Algorithm: "argon2id" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM - # Time: 1 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_TIME - # Memory: 65536 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_MEMORY - # Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS - - # Hasher: - # Algorithm: "scrypt" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM - # Cost: 15 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST - - # Hasher: - # Algorithm: "pbkdf2" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM - # Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ROUNDS - # Hash: "sha256" # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_HASH + # Time takes effect for the algorithms argon2i and argon2id + Time: 3 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_TIME + # Memory takes effect for the algorithms argon2i and argon2id + Memory: 32768 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_MEMORY + # Threads takes effect for the algorithms argon2i and argon2id + Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS + # Rounds takes effect for the algorithm pbkdf2 + Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ROUNDS + # Hash takes effect for the algorithm pbkdf2 + # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" + Hash: sha256 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_HASH # Verifiers enable the possibility of verifying # passwords that are previously hashed using another @@ -478,7 +471,7 @@ SystemDefaults: # https://passlib.readthedocs.io/en/stable/modular_crypt_format.html # # Supported verifiers: (uncomment to enable) - Verifiers: + Verifiers: # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_VERIFIERS # - "argon2" # verifier for both argon2i and argon2id. # - "bcrypt" # - "md5" @@ -486,11 +479,24 @@ SystemDefaults: # - "pbkdf2" # verifier for all pbkdf2 hash modes. SecretHasher: # Set hasher configuration for machine users, API and OIDC client secrets. - # See PasswordHasher for all possible options Hasher: - Algorithm: "bcrypt" # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_ALGORITHM + # Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2" + # Depending on the algorithm, different configuration options take effect. + Algorithm: bcrypt + # Cost takes effect for the algorithms bcrypt and scrypt Cost: 4 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_COST - Verifiers: + # Time takes effect for the algorithms argon2i and argon2id + Time: 3 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_TIME + # Memory takes effect for the algorithms argon2i and argon2id + Memory: 32768 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_MEMORY + # Threads takes effect for the algorithms argon2i and argon2id + Threads: 4 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_THREADS + # Rounds takes effect for the algorithm pbkdf2 + Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_ROUNDS + # Hash takes effect for the algorithm pbkdf2 + # Can be "sha1", "sha224", "sha256", "sha384" or "sha512" + Hash: sha256 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_HASH + Verifiers: # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_VERIFIERS Multifactors: OTP: # If this is empty, the issuer is the requested domain diff --git a/docs/docs/apis/actionsv2/execution-local.md b/docs/docs/apis/actionsv2/execution-local.md new file mode 100644 index 0000000000..3f0ccc0fe0 --- /dev/null +++ b/docs/docs/apis/actionsv2/execution-local.md @@ -0,0 +1,139 @@ +--- +title: Actions v2 example execution locally +--- + +In this guide, you will create a ZITADEL execution and target. After a user is created through the API, the target is called. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +## Start example target + +To start a simple HTTP server locally, which receives the webhook call, the following code example can be used: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +What happens here is only a target which prints out the received request, which could also be handled with a different logic. + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +[Create a target](/apis/resources/action_service_v3/action-service-create-target) + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v3alpha/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To call the target just created before, with the intention to print the request used for user creation by the user V2 API, we define an execution with a method condition. + +[Set an execution](/apis/resources/action_service_v3/action-service-set-execution) + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "request": { + "method": "/zitadel.user.v2beta.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now on every call on `/zitadel.user.v2beta.UserService/AddHumanUser` the local server prints out the received body of the request: + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } +}' +``` + +Should print out something like, also described under [Sent information Request](./introduction#sent-information-request): +```shell +{ + "fullMethod": "/zitadel.user.v2beta.UserService/AddHumanUser", + "instanceID": "262851882718855632", + "orgID": "262851882718921168", + "projectID": "262851882719052240", + "userID": "262851882718986704", + "request": { + "profile": { + "given_name": "Example_given", + "family_name": "Example_family" + }, + "email": { + "email": "example@example.com" + } + } +} +``` + + diff --git a/docs/docs/apis/actionsv2/introduction.md b/docs/docs/apis/actionsv2/introduction.md new file mode 100644 index 0000000000..16adaac423 --- /dev/null +++ b/docs/docs/apis/actionsv2/introduction.md @@ -0,0 +1,167 @@ +--- +title: Actions V2 +--- + +This page describes the options you have when defining ZITADEL Actions V2. + +## Endpoints + +ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled. + +### Sent information Request + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": "full request of the call" +} +``` + +### Sent information Response + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": "full request of the call", + "response": "full response of the call" +} +``` + +## Target + +The Target describes how ZITADEL interacts with the Endpoint. + +There are different types of Targets: + +- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError +- `Call`, the call handles the status code and response, can be InterruptOnError +- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets + +`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore. + +The API documentation to create a target can be found [here](/apis/resources/action_service_v3/action-service-create-target) + +## Execution + +ZITADEL decides on specific conditions if one or more Targets have to be called. +The Execution resource contains 2 parts, the condition and the called targets. + +The condition can be defined for 4 types of processes: + +- `Requests`, before a request is processed by ZITADEL +- `Responses`, before a response is sent back to the application +- `Functions`, handling specific functionality in the logic of ZITADEL +- `Events`, after a specific event happened and was stored in ZITADEL + +The API documentation to set an Execution can be found [here](/apis/resources/action_service_v3/action-service-set-execution) + +### Condition Best Match + +As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. +This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2beta.UserService` and on `/zitadel.user.v2beta.UserService/AddHumanUser`, +ZITADEL would with a call on the `/zitadel.user.v2beta.UserService/AddHumanUser` use the Executions with the following priority: + +1. `/zitadel.user.v2beta.UserService/AddHumanUser` +2. `zitadel.user.v2beta.UserService` +3. `all` + +If you then have a call on `/zitadel.user.v2beta.UserService/UpdateHumanUser` the following priority would be found: + +1. `zitadel.user.v2beta.UserService` +2. `all` + +And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. + +### Targets and Includes + +An execution can not only contain a list of Targets, but also Includes. +The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. + +If you define 2 Executions as follows: + +```json +{ + "condition": { + "request": { + "service": "zitadel.user.v2beta.UserService" + } + }, + "targets": [ + { + "target": "" + } + ] +} +``` + +```json +{ + "condition": { + "request": { + "method": "/zitadel.user.v2beta.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + }, + { + "include": { + "request": { + "service": "zitadel.user.v2beta.UserService" + } + } + } + ] +} +``` + +The called Targets on "/zitadel.user.v2beta.UserService/AddHumanUser" would be, in order: + +1. `` +2. `` + +### Condition for Requests and Responses + +For Request and Response there are 3 levels the condition can be defined: + +- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API +- `Service`, handling any request or response under a service of the ZITADEL API +- `All`, handling any request or response under the ZITADEL API + +The available conditions can be found under: +- [All available Methods](/apis/resources/action_service_v3/action-service-list-execution-methods), for example `/zitadel.user.v2beta.UserService/AddHumanUser` +- [All available Services](/apis/resources/action_service_v3/action-service-list-execution-services), for example `zitadel.user.v2beta.UserService` + +### Condition for Functions + +Replace the current Actions with the following flows: + +- [Internal Authentication](../actions/internal-authentication) +- [External Authentication](../actions/external-authentication) +- [Complement Token](../actions/complement-token) +- [Customize SAML Response](../actions/customize-samlresponse) + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v3/action-service-list-execution-functions). + +### Condition for Events + +For event there are 3 levels the condition can be defined: + +- Event, handling a specific event +- Group, handling a specific group of events +- All, handling any event in ZITADEL + +The concept of events can be found under [Events](/concepts/architecture/software#events) \ No newline at end of file diff --git a/docs/docs/concepts/features/actions.md b/docs/docs/concepts/features/actions.md index c3c18b0611..21626d237c 100644 --- a/docs/docs/concepts/features/actions.md +++ b/docs/docs/concepts/features/actions.md @@ -7,7 +7,7 @@ By using ZITADEL actions, you can manipulate ZITADELs behavior on specific Event This is useful when you have special business requirements that ZITADEL doesn't support out-of-the-box. :::info -We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. +We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. ::: ## Why actions? diff --git a/docs/docs/concepts/features/actions_v2.md b/docs/docs/concepts/features/actions_v2.md new file mode 100644 index 0000000000..3c78229045 --- /dev/null +++ b/docs/docs/concepts/features/actions_v2.md @@ -0,0 +1,40 @@ +--- +title: ZITADEL Actions v2 +sidebar_label: Actions v2 +--- + +By using ZITADEL actions V2, you can manipulate ZITADELs behavior on specific API calls, events or functions. +This is useful when you have special business requirements that ZITADEL doesn't support out-of-the-box. + +:::info +We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. +::: + +## Why actions? +ZITADEL can't anticipate and solve every possible business rule and integration requirements from all ZITADEL users. Here are some examples: +- A business requires domain specific data validation before a user can be created or authenticated. +- A business needs to automate tasks. Roles should be assigned to users based on their ADFS 2016+ groups. +- A business needs to store metadata on a user that is used for integrating applications. +- A business needs to restrict the users who are allowed to register to a certain organization by their email domains. + +With actions, ZITADEL provides a way to solve such problems. + +## How it works +There are 3 components necessary: +- Endpoint, an external endpoint with the desired logic, can be whatever is necessary as long as it can receive an HTTP Post request. +- Target, a resource in ZITADEL with all necessary information how to trigger an endpoint +- Execution, a resource in ZITADEL with the information when to trigger which targets + +The process is that ZITADEL decides at certain points that with a defined Execution a call to the defined Target(s) is triggered, +so that everybody can implement their custom behaviour for as many processes as possible. + +Possible conditions for the Execution: +- Request, to react to or manipulate requests to ZITADEL, for example add information to newly created users +- Response, to react to or manipulate responses to ZITADEL, for example to provision newly created users to other systems +- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions) +- Event, to create to different events which get created in ZITADEL, for example to inform somebody if a user gets locked + +## Further reading + +- [Actions v2 example execution locally](/apis/actionsv2/execution-local) +- [Actions v2 reference](/apis/actionsv2/introduction#action) \ No newline at end of file diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index c2b7773789..4a0090a7c0 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -15,8 +15,9 @@ To access default settings, use the settomgs page at `{instanceDomain}/ui/consol When you configure your default settings, you can set the following: -- **General**: Default Language for the UI -- [**Notification settings**](#notification-providers-and-smtp): Notification and Email Server settings, so initialization-, verification- and other mails are sent from your own domain. For SMS, Twilio is supported as notification provider. +- **Organizations**: A list of your organizations +- [**Features**](#features): Feature Settings let you try out new features before they become generally available. You can also disable features you are not interested in. +- [**Notification settings**](#notification-providers-and-smtp): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. - [**Login Behavior and Access**](#login-behavior-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations - [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. @@ -25,10 +26,30 @@ When you configure your default settings, you can set the following: - [**Branding**](#branding): Appearance of the login interface. - [**Message Texts**](#message-texts): Text and internationalization for emails - [**Login Interface Texts**](#login-interface-texts): Text and internationalization for the login interface +- [**Languages**](#languages): Select which supported langauges are shown to your users. Set the default language if no context is provided. - [**Privacy Policy**](#privacy-policy-and-tos): Links to your own Terms of Service and Privacy Policy regulations. Link to Help Page. - [**OIDC Token Lifetimes and Expiration**](#oidc-token-lifetimes-and-expiration): Token lifetime and expiration settings. - [**Secret Generator**](#secret-generator): Appearance and expiration of the generated codes and secrets used in mails for verification etc. +## Features + +Feature Settings let you try out new features before they become generally available. You can also disable features you are not interested in. + +The Page lets you choose between the settings `Enabled`, `Disabled` or `Inherit`. +If a feature is set to `Inherit`, it becomes available once its enabled per default. + +Feature page + +Features can range from UI changes in the console, to new APIs or performance improvements. + +:::warning +Be careful on which features you enable as they can be in an experimental state. +::: + ## Branding We recommend setting your Branding and SMTP settings initially as it will comfort your customers having a familiar UI for login and receiving notifications from your domain and mail addresses. @@ -85,13 +106,13 @@ To configure your custom SMTP please fill the following fields: - User - SMTP Password -SMTP +SMTP ### SMS No default provider is configured to send some SMS to your users. If you like to validate the phone numbers of your users make sure to add your twilio configuration by adding your Sid, Token and Sender Number. -Twilio +Twilio ## Login Behavior and Access @@ -111,7 +132,7 @@ The Login Policy defines how the login process should look like and which authen Login Bahaviour and Access ### Default Redirect URI @@ -130,7 +151,11 @@ We recommend setting your own default redirect URI, if you do not want end users Change default redirect url of instance: https:///ui/console/settings?id=login -![Login Policy Advanced Setting: Default Redirect URL](/img/guides/solution-scenarios/console-default-redirect.png) +Login Policy Advanced Setting: Default Redirect URL ### Passwordless @@ -182,7 +207,7 @@ The following properties can be set: - Has Uppercase - Has Lowercase - Has Number -- Has Symbol +- Has Symbol (Everything that is not a number or letter) +## Languages + +Drag allowed languages to the left column. Languages in the right column are not shown to your users. + +Choose a default language which acts as a fallback, if no language header is set. + +Languages + ## OIDC token lifetimes and expiration Configure how long the different oidc tokens should life. diff --git a/docs/sidebars.js b/docs/sidebars.js index ddb73badde..48eda263da 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -795,6 +795,15 @@ module.exports = { "apis/actions/objects", ], }, + { + type: "category", + label: "Actions V2", + collapsed: false, + items: [ + "apis/actionsv2/introduction", + "apis/actionsv2/execution-local", + ], + }, { type: "doc", label: "gRPC Status Codes", diff --git a/docs/static/img/console_feature_settings.png b/docs/static/img/console_feature_settings.png new file mode 100644 index 0000000000..b8bcbeaa40 Binary files /dev/null and b/docs/static/img/console_feature_settings.png differ diff --git a/docs/static/img/console_languages.png b/docs/static/img/console_languages.png new file mode 100644 index 0000000000..750f970ddd Binary files /dev/null and b/docs/static/img/console_languages.png differ diff --git a/go.mod b/go.mod index 9fdc3a7cf6..cb443c0053 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,6 @@ module github.com/zitadel/zitadel -go 1.22 - -// https://go.dev/doc/toolchain -toolchain go1.22.2 +go 1.22.2 require ( cloud.google.com/go/storage v1.40.0 diff --git a/internal/api/grpc/action/v3alpha/execution.go b/internal/api/grpc/action/v3alpha/execution.go index dde4727c2e..58a36cff22 100644 --- a/internal/api/grpc/action/v3alpha/execution.go +++ b/internal/api/grpc/action/v3alpha/execution.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/repository/execution" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" ) @@ -33,46 +34,46 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque return nil, err } + targets := make([]*execution.Target, len(req.Targets)) + for i, target := range req.Targets { + switch t := target.GetType().(type) { + case *action.ExecutionTargetType_Include: + include, err := conditionToInclude(t.Include) + if err != nil { + return nil, err + } + targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include} + case *action.ExecutionTargetType_Target: + targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target} + } + } set := &command.SetExecution{ - Targets: req.GetTargets(), - Includes: req.GetIncludes(), + Targets: targets, } var err error var details *domain.ObjectDetails switch t := req.GetCondition().GetConditionType().(type) { case *action.Condition_Request: - cond := &command.ExecutionAPICondition{ - Method: t.Request.GetMethod(), - Service: t.Request.GetService(), - All: t.Request.GetAll(), - } + cond := executionConditionFromRequest(t.Request) details, err = s.command.SetExecutionRequest(ctx, cond, set, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } case *action.Condition_Response: - cond := &command.ExecutionAPICondition{ - Method: t.Response.GetMethod(), - Service: t.Response.GetService(), - All: t.Response.GetAll(), - } + cond := executionConditionFromResponse(t.Response) details, err = s.command.SetExecutionResponse(ctx, cond, set, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } case *action.Condition_Event: - cond := &command.ExecutionEventCondition{ - Event: t.Event.GetEvent(), - Group: t.Event.GetGroup(), - All: t.Event.GetAll(), - } + cond := executionConditionFromEvent(t.Event) details, err = s.command.SetExecutionEvent(ctx, cond, set, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } case *action.Condition_Function: - details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), set, authz.GetInstance(ctx).InstanceID()) + details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } @@ -82,6 +83,36 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque }, nil } +func conditionToInclude(cond *action.Condition) (string, error) { + switch t := cond.GetConditionType().(type) { + case *action.Condition_Request: + cond := executionConditionFromRequest(t.Request) + if err := cond.IsValid(); err != nil { + return "", err + } + return cond.ID(domain.ExecutionTypeRequest), nil + case *action.Condition_Response: + cond := executionConditionFromResponse(t.Response) + if err := cond.IsValid(); err != nil { + return "", err + } + return cond.ID(domain.ExecutionTypeRequest), nil + case *action.Condition_Event: + cond := executionConditionFromEvent(t.Event) + if err := cond.IsValid(); err != nil { + return "", err + } + return cond.ID(), nil + case *action.Condition_Function: + cond := command.ExecutionFunctionCondition(t.Function.GetName()) + if err := cond.IsValid(); err != nil { + return "", err + } + return cond.ID(), nil + } + return "", nil +} + func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutionRequest) (*action.DeleteExecutionResponse, error) { if err := checkExecutionEnabled(ctx); err != nil { return nil, err @@ -91,37 +122,25 @@ func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutio var details *domain.ObjectDetails switch t := req.GetCondition().GetConditionType().(type) { case *action.Condition_Request: - cond := &command.ExecutionAPICondition{ - Method: t.Request.GetMethod(), - Service: t.Request.GetService(), - All: t.Request.GetAll(), - } + cond := executionConditionFromRequest(t.Request) details, err = s.command.DeleteExecutionRequest(ctx, cond, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } case *action.Condition_Response: - cond := &command.ExecutionAPICondition{ - Method: t.Response.GetMethod(), - Service: t.Response.GetService(), - All: t.Response.GetAll(), - } + cond := executionConditionFromResponse(t.Response) details, err = s.command.DeleteExecutionResponse(ctx, cond, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } case *action.Condition_Event: - cond := &command.ExecutionEventCondition{ - Event: t.Event.GetEvent(), - Group: t.Event.GetGroup(), - All: t.Event.GetAll(), - } + cond := executionConditionFromEvent(t.Event) details, err = s.command.DeleteExecutionEvent(ctx, cond, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } case *action.Condition_Function: - details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), authz.GetInstance(ctx).InstanceID()) + details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } @@ -130,3 +149,27 @@ func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutio Details: object.DomainToDetailsPb(details), }, nil } + +func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition { + return &command.ExecutionAPICondition{ + Method: request.GetMethod(), + Service: request.GetService(), + All: request.GetAll(), + } +} + +func executionConditionFromResponse(response *action.ResponseExecution) *command.ExecutionAPICondition { + return &command.ExecutionAPICondition{ + Method: response.GetMethod(), + Service: response.GetService(), + All: response.GetAll(), + } +} + +func executionConditionFromEvent(event *action.EventExecution) *command.ExecutionEventCondition { + return &command.ExecutionEventCondition{ + Event: event.GetEvent(), + Group: event.GetGroup(), + All: event.GetAll(), + } +} diff --git a/internal/api/grpc/action/v3alpha/execution_integration_test.go b/internal/api/grpc/action/v3alpha/execution_integration_test.go index 752e9b546a..4ca1b97d6f 100644 --- a/internal/api/grpc/action/v3alpha/execution_integration_test.go +++ b/internal/api/grpc/action/v3alpha/execution_integration_test.go @@ -9,14 +9,23 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" ) +func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType { + return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}} +} + +func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType { + return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}} +} + func TestServer_SetExecution_Request(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -48,7 +57,7 @@ func TestServer_SetExecution_Request(t *testing.T) { Request: &action.RequestExecution{}, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -65,7 +74,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -82,7 +91,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -104,7 +113,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -121,7 +130,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -143,7 +152,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -163,27 +172,28 @@ func TestServer_SetExecution_Request(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) }) } } func TestServer_SetExecution_Request_Include(t *testing.T) { ensureFeatureEnabled(t) - - targetResp := Tester.CreateTarget(CTX, t) - executionCond := "request" - Tester.SetExecution(CTX, t, - &action.Condition{ - ConditionType: &action.Condition_Request{ - Request: &action.RequestExecution{ - Condition: &action.RequestExecution_All{ - All: true, - }, + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + executionCond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{ + All: true, }, }, }, - []string{targetResp.GetId()}, - []string{}, + } + Tester.SetExecution(CTX, t, + executionCond, + executionTargetsSingleTarget(targetResp.GetId()), ) tests := []struct { @@ -206,7 +216,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, }, - Includes: []string{executionCond}, + Targets: executionTargetsSingleInclude(executionCond), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -228,7 +238,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, }, - Includes: []string{executionCond}, + Targets: executionTargetsSingleInclude(executionCond), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -237,6 +247,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, }, + /* circular { name: "all, ok", ctx: CTX, @@ -250,7 +261,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, }, - Includes: []string{executionCond}, + Targets: executionTargetsSingleInclude(executionCond), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -259,6 +270,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { }, }, }, + */ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -270,13 +282,16 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) }) } } func TestServer_DeleteExecution_Request(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -332,7 +347,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "method, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -373,7 +388,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "service, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -398,7 +413,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "all, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -441,7 +456,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) { func TestServer_SetExecution_Response(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -473,7 +488,7 @@ func TestServer_SetExecution_Response(t *testing.T) { Response: &action.ResponseExecution{}, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -490,7 +505,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -507,7 +522,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -529,7 +544,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -546,7 +561,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -568,7 +583,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -588,13 +603,16 @@ func TestServer_SetExecution_Response(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) }) } } func TestServer_DeleteExecution_Response(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -652,7 +670,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "method, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -693,7 +711,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "service, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -718,7 +736,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "all, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -761,7 +779,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) { func TestServer_SetExecution_Event(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -795,7 +813,7 @@ func TestServer_SetExecution_Event(t *testing.T) { Event: &action.EventExecution{}, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -833,7 +851,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -876,7 +894,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -898,7 +916,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -918,13 +936,16 @@ func TestServer_SetExecution_Event(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) }) } } func TestServer_DeleteExecution_Event(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -985,7 +1006,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "event, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -1026,7 +1047,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "group, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -1061,18 +1082,13 @@ func TestServer_DeleteExecution_Event(t *testing.T) { }, }, }, - want: &action.DeleteExecutionResponse{ - Details: &object.Details{ - ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Instance.InstanceID(), - }, - }, + wantErr: true, }, { name: "all, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ @@ -1115,7 +1131,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) { func TestServer_SetExecution_Function(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -1147,7 +1163,7 @@ func TestServer_SetExecution_Function(t *testing.T) { Response: &action.ResponseExecution{}, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -1157,10 +1173,10 @@ func TestServer_SetExecution_Function(t *testing.T) { req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Function{ - Function: "xxx", + Function: &action.FunctionExecution{Name: "xxx"}, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, wantErr: true, }, @@ -1170,10 +1186,10 @@ func TestServer_SetExecution_Function(t *testing.T) { req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Function{ - Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication", + Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, }, }, - Targets: []string{targetResp.GetId()}, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, want: &action.SetExecutionResponse{ Details: &object.Details{ @@ -1193,13 +1209,16 @@ func TestServer_SetExecution_Function(t *testing.T) { require.NoError(t, err) integration.AssertDetails(t, tt.want, got) + + // cleanup to not impact other requests + Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) }) } } func TestServer_DeleteExecution_Function(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { name string @@ -1243,7 +1262,7 @@ func TestServer_DeleteExecution_Function(t *testing.T) { req: &action.DeleteExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Function{ - Function: "xxx", + Function: &action.FunctionExecution{Name: "xxx"}, }, }, }, @@ -1253,13 +1272,13 @@ func TestServer_DeleteExecution_Function(t *testing.T) { name: "function, ok", ctx: CTX, dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) + Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId())) return nil }, req: &action.DeleteExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Function{ - Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication", + Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}, }, }, }, diff --git a/internal/api/grpc/action/v3alpha/execution_target_integration_test.go b/internal/api/grpc/action/v3alpha/execution_target_integration_test.go new file mode 100644 index 0000000000..30afb1af6f --- /dev/null +++ b/internal/api/grpc/action/v3alpha/execution_target_integration_test.go @@ -0,0 +1,323 @@ +//go:build integration + +package action_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" +) + +func TestServer_ExecutionTarget(t *testing.T) { + ensureFeatureEnabled(t) + + fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID" + + tests := []struct { + name string + ctx context.Context + dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) (func(), error) + clean func(context.Context) + req *action.GetTargetByIDRequest + want *action.GetTargetByIDResponse + wantErr bool + }{ + { + name: "GetTargetByID, request and response, ok", + ctx: CTX, + dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) { + + instanceID := Tester.Instance.InstanceID() + orgID := Tester.Organisation.ID + projectID := "" + userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID + + // create target for target changes + targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1) + targetCreatedURL := "https://nonexistent" + + targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} + changedRequest := &action.GetTargetByIDRequest{TargetId: targetCreated.GetId()} + // replace original request with different targetID + urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest) + targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, false) + Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) + // GetTargetByID with used target + request.TargetId = targetRequest.GetId() + + // expected response from the GetTargetByID + expectedResponse := &action.GetTargetByIDResponse{ + Target: &action.Target{ + TargetId: targetCreated.GetId(), + Details: targetCreated.GetDetails(), + Name: targetCreatedName, + Endpoint: targetCreatedURL, + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + } + // has to be set separately because of the pointers + response.Target = &action.Target{ + TargetId: targetCreated.GetId(), + Details: targetCreated.GetDetails(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + } + + // content for partial update + changedResponse := &action.GetTargetByIDResponse{ + Target: &action.Target{ + TargetId: "changed", + }, + } + // change partial updated content on returned response + response.Target.TargetId = changedResponse.Target.TargetId + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instanceID, + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: changedRequest, + Response: expectedResponse, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse) + targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, false) + Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) + + return func() { + closeRequest() + closeResponse() + }, nil + }, + clean: func(ctx context.Context) { + Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetByIDRequest{}, + want: &action.GetTargetByIDResponse{}, + }, + /*{ + name: "GetTargetByID, request, interrupt", + ctx: CTX, + dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) { + + fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID" + instanceID := Tester.Instance.InstanceID() + orgID := Tester.Organisation.ID + projectID := "" + userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request} + urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetByIDRequest{TargetId: "notchanged"}) + + targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, true) + Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId())) + // GetTargetByID with used target + request.TargetId = targetRequest.GetId() + + return func() { + closeRequest() + }, nil + }, + clean: func(ctx context.Context) { + Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + }, + req: &action.GetTargetByIDRequest{}, + wantErr: true, + }, + { + name: "GetTargetByID, response, interrupt", + ctx: CTX, + dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) { + + fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID" + instanceID := Tester.Instance.InstanceID() + orgID := Tester.Organisation.ID + projectID := "" + userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID + + // create target for target changes + targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1) + targetCreatedURL := "https://nonexistent" + + targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // GetTargetByID with used target + request.TargetId = targetCreated.GetId() + + // expected response from the GetTargetByID + expectedResponse := &action.GetTargetByIDResponse{ + Target: &action.Target{ + TargetId: targetCreated.GetId(), + Details: targetCreated.GetDetails(), + Name: targetCreatedName, + Endpoint: targetCreatedURL, + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + } + + // content for partial update + changedResponse := &action.GetTargetByIDResponse{ + Target: &action.Target{ + TargetId: "changed", + }, + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instanceID, + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: request, + Response: expectedResponse, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse) + targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, true) + Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId())) + + return func() { + closeResponse() + }, nil + }, + clean: func(ctx context.Context) { + Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetByIDRequest{}, + wantErr: true, + },*/ + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dep != nil { + close, err := tt.dep(tt.ctx, tt.req, tt.want) + require.NoError(t, err) + defer close() + } + + got, err := Client.GetTargetByID(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget()) + + assert.Equal(t, tt.want.Target.TargetId, got.Target.TargetId) + + if tt.clean != nil { + tt.clean(tt.ctx) + } + }) + } +} + +func conditionRequestFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionResponseFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func testServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (string, func()) { + handler := func(w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError) + return + } + + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError) + return + } + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError) + return + } + if statusCode != http.StatusOK { + http.Error(w, "error, statusCode", statusCode) + return + } + + time.Sleep(sleep) + + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.WriteString(w, string(resp)); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + + return server.URL, server.Close +} diff --git a/internal/api/grpc/action/v3alpha/query.go b/internal/api/grpc/action/v3alpha/query.go index 582510b0bf..095eaa7973 100644 --- a/internal/api/grpc/action/v3alpha/query.go +++ b/internal/api/grpc/action/v3alpha/query.go @@ -2,6 +2,7 @@ package action import ( "context" + "strings" "google.golang.org/protobuf/types/known/durationpb" @@ -67,8 +68,6 @@ func targetFieldNameToSortingColumn(field action.TargetFieldName) query.Column { return query.TargetColumnURL case action.TargetFieldName_FIELD_NAME_TIMEOUT: return query.TargetColumnTimeout - case action.TargetFieldName_FIELD_NAME_ASYNC: - return query.TargetColumnAsync case action.TargetFieldName_FIELD_NAME_INTERRUPT_ON_ERROR: return query.TargetColumnInterruptOnError default: @@ -134,19 +133,16 @@ func targetToPb(t *query.Target) *action.Target { TargetId: t.ID, Name: t.Name, Timeout: durationpb.New(t.Timeout), - } - if t.Async { - target.ExecutionType = &action.Target_IsAsync{IsAsync: t.Async} - } - if t.InterruptOnError { - target.ExecutionType = &action.Target_InterruptOnError{InterruptOnError: t.InterruptOnError} + Endpoint: t.Endpoint, } switch t.TargetType { case domain.TargetTypeWebhook: - target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{Url: t.URL}} - case domain.TargetTypeRequestResponse: - target.TargetType = &action.Target_RestRequestResponse{RestRequestResponse: &action.SetRESTRequestResponse{Url: t.URL}} + target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}} + case domain.TargetTypeCall: + target.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}} + case domain.TargetTypeAsync: + target.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}} default: target.TargetType = nil } @@ -205,10 +201,14 @@ func executionQueryToQuery(searchQuery *action.SearchQuery) (query.SearchQuery, return inConditionsQueryToQuery(q.InConditionsQuery) case *action.SearchQuery_ExecutionTypeQuery: return executionTypeToQuery(q.ExecutionTypeQuery) - case *action.SearchQuery_TargetQuery: - return query.NewExecutionTargetSearchQuery(q.TargetQuery.GetTargetId()) case *action.SearchQuery_IncludeQuery: - return query.NewExecutionIncludeSearchQuery(q.IncludeQuery.GetInclude()) + include, err := conditionToInclude(q.IncludeQuery.GetInclude()) + if err != nil { + return nil, err + } + return query.NewIncludeSearchQuery(include) + case *action.SearchQuery_TargetQuery: + return query.NewTargetSearchQuery(q.TargetQuery.GetTargetId()) default: return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") } @@ -267,7 +267,7 @@ func conditionToID(q *action.Condition) (string, error) { } return cond.ID(), nil case *action.Condition_Function: - return t.Function, nil + return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil default: return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") } @@ -282,17 +282,83 @@ func executionsToPb(executions []*query.Execution) []*action.Execution { } func executionToPb(e *query.Execution) *action.Execution { - var targets, includes []string - if len(e.Targets) > 0 { - targets = e.Targets - } - if len(e.Includes) > 0 { - includes = e.Includes + targets := make([]*action.ExecutionTargetType, len(e.Targets)) + for i := range e.Targets { + switch e.Targets[i].Type { + case domain.ExecutionTargetTypeInclude: + targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}} + case domain.ExecutionTargetTypeTarget: + targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}} + case domain.ExecutionTargetTypeUnspecified: + continue + default: + continue + } } + return &action.Execution{ - Details: object.DomainToDetailsPb(&e.ObjectDetails), - ExecutionId: e.ID, - Targets: targets, - Includes: includes, + Details: object.DomainToDetailsPb(&e.ObjectDetails), + Condition: executionIDToCondition(e.ID), + Targets: targets, } } + +func executionIDToCondition(include string) *action.Condition { + if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) { + return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) { + return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) { + return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) { + return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String())) + } + return nil +} + +func includeRequestToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case 2: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}} + case 1: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} + case 0: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}} + default: + return nil + } +} +func includeResponseToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case 2: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}} + case 1: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} + case 0: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}} + default: + return nil + } +} + +func includeEventToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case 1: + if strings.HasSuffix(id, command.EventGroupSuffix) { + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}} + } else { + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}} + } + case 0: + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}} + default: + return nil + } +} + +func includeFunctionToCondition(id string) *action.Condition { + return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}} +} diff --git a/internal/api/grpc/action/v3alpha/query_integration_test.go b/internal/api/grpc/action/v3alpha/query_integration_test.go index 31b5a8025a..b083eda5a4 100644 --- a/internal/api/grpc/action/v3alpha/query_integration_test.go +++ b/internal/api/grpc/action/v3alpha/query_integration_test.go @@ -5,6 +5,7 @@ package action_test import ( "context" "fmt" + "reflect" "testing" "time" @@ -12,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/durationpb" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" @@ -52,7 +54,7 @@ func TestServer_GetTargetByID(t *testing.T) { ctx: CTX, dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) request.TargetId = resp.GetId() response.Target.TargetId = resp.GetId() @@ -69,10 +71,9 @@ func TestServer_GetTargetByID(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, + Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, + RestWebhook: &action.SetRESTWebhook{}, }, Timeout: durationpb.New(10 * time.Second), }, @@ -84,7 +85,7 @@ func TestServer_GetTargetByID(t *testing.T) { ctx: CTX, dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTargetWithNameAndType(ctx, t, name, true, false) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) request.TargetId = resp.GetId() response.Target.TargetId = resp.GetId() @@ -101,23 +102,21 @@ func TestServer_GetTargetByID(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.SetRESTAsync{}, }, - Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.Target_IsAsync{IsAsync: true}, + Timeout: durationpb.New(10 * time.Second), }, }, }, { - name: "get, interruptOnError, ok", + name: "get, webhook interruptOnError, ok", args: args{ ctx: CTX, dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, true) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) request.TargetId = resp.GetId() response.Target.TargetId = resp.GetId() @@ -134,13 +133,79 @@ func TestServer_GetTargetByID(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, + Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + InterruptOnError: true, }, }, - Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true}, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + { + name: "get, call, ok", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) + request.TargetId = resp.GetId() + + response.Target.TargetId = resp.GetId() + response.Target.Name = name + response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() + response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Target.Details.Sequence = resp.GetDetails().GetSequence() + return nil + }, + req: &action.GetTargetByIDRequest{}, + }, + want: &action.GetTargetByIDResponse{ + Target: &action.Target{ + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + { + name: "get, call interruptOnError, ok", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) + request.TargetId = resp.GetId() + + response.Target.TargetId = resp.GetId() + response.Target.Name = name + response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() + response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Target.Details.Sequence = resp.GetDetails().GetSequence() + return nil + }, + req: &action.GetTargetByIDRequest{}, + }, + want: &action.GetTargetByIDResponse{ + Target: &action.Target{ + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), }, }, }, @@ -163,15 +228,11 @@ func TestServer_GetTargetByID(t *testing.T) { assert.Error(ttt, getErr, "Error: "+getErr.Error()) } else { assert.NoError(ttt, getErr) - } - if getErr != nil { - fmt.Println("Error: " + getErr.Error()) - return - } - integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget()) + integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget()) - assert.Equal(t, tt.want.Target, got.Target) + assert.Equal(t, tt.want.Target, got.Target) + } }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") }) @@ -227,14 +288,14 @@ func TestServer_ListTargets(t *testing.T) { ctx: CTX, dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error { name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{ InTargetIdsQuery: &action.InTargetIDsQuery{ TargetIds: []string{resp.GetId()}, }, } response.Details.Timestamp = resp.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + //response.Details.ProcessedSequence = resp.GetDetails().GetSequence() response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() @@ -255,9 +316,10 @@ func TestServer_ListTargets(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, + Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + InterruptOnError: false, }, }, Timeout: durationpb.New(10 * time.Second), @@ -270,7 +332,7 @@ func TestServer_ListTargets(t *testing.T) { ctx: CTX, dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error { name := fmt.Sprint(time.Now().UnixNano() + 1) - resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false) + resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) request.Queries[0].Query = &action.TargetSearchQuery_TargetNameQuery{ TargetNameQuery: &action.TargetNameQuery{ TargetName: name, @@ -298,9 +360,10 @@ func TestServer_ListTargets(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, + Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + InterruptOnError: false, }, }, Timeout: durationpb.New(10 * time.Second), @@ -316,9 +379,9 @@ func TestServer_ListTargets(t *testing.T) { name1 := fmt.Sprint(time.Now().UnixNano() + 1) name2 := fmt.Sprint(time.Now().UnixNano() + 3) name3 := fmt.Sprint(time.Now().UnixNano() + 5) - resp1 := Tester.CreateTargetWithNameAndType(ctx, t, name1, false, false) - resp2 := Tester.CreateTargetWithNameAndType(ctx, t, name2, true, false) - resp3 := Tester.CreateTargetWithNameAndType(ctx, t, name3, false, true) + resp1 := Tester.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) + resp2 := Tester.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) + resp3 := Tester.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{ InTargetIdsQuery: &action.InTargetIDsQuery{ TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, @@ -354,9 +417,10 @@ func TestServer_ListTargets(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, + Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + InterruptOnError: false, }, }, Timeout: durationpb.New(10 * time.Second), @@ -365,25 +429,23 @@ func TestServer_ListTargets(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, }, }, - Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.Target_IsAsync{IsAsync: true}, + Timeout: durationpb.New(10 * time.Second), }, { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - TargetType: &action.Target_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.SetRESTAsync{}, }, - Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true}, + Timeout: durationpb.New(10 * time.Second), }, }, }, @@ -422,9 +484,9 @@ func TestServer_ListTargets(t *testing.T) { } } -func TestServer_ListExecutions_Request(t *testing.T) { +func TestServer_ListExecutions(t *testing.T) { ensureFeatureEnabled(t) - targetResp := Tester.CreateTarget(CTX, t) + targetResp := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) type args struct { ctx context.Context @@ -446,17 +508,20 @@ func TestServer_ListExecutions_Request(t *testing.T) { wantErr: true, }, { - name: "list single condition", + name: "list request single condition", args: args{ ctx: CTX, dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - resp := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{}) + cond := request.Queries[0].GetInConditionsQuery().GetConditions()[0] + resp := Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) response.Details.Timestamp = resp.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + // response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + // Set expected response with used values for SetExecution response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() + response.Result[0].Condition = cond return nil }, req: &action.ListExecutionsRequest{ @@ -471,8 +536,7 @@ func TestServer_ListExecutions_Request(t *testing.T) { }, }, }, - }, - }, + }}, }, }, }}, @@ -487,18 +551,26 @@ func TestServer_ListExecutions_Request(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession", - Targets: []string{targetResp.GetId()}, + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/GetSession", + }, + }, + }, + }, + Targets: executionTargetsSingleTarget(targetResp.GetId()), }, }, }, }, { - name: "list single target", + name: "list request single target", args: args{ ctx: CTX, dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - target := Tester.CreateTarget(ctx, t) + target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) // add target as query to the request request.Queries[0] = &action.SearchQuery{ Query: &action.SearchQuery_TargetQuery{ @@ -507,7 +579,7 @@ func TestServer_ListExecutions_Request(t *testing.T) { }, }, } - resp := Tester.SetExecution(ctx, t, &action.Condition{ + cond := &action.Condition{ ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ @@ -515,14 +587,17 @@ func TestServer_ListExecutions_Request(t *testing.T) { }, }, }, - }, []string{target.GetId()}, []string{}) + } + targets := executionTargetsSingleTarget(target.GetId()) + resp := Tester.SetExecution(ctx, t, cond, targets) response.Details.Timestamp = resp.GetDetails().GetChangeDate() response.Details.ProcessedSequence = resp.GetDetails().GetSequence() response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() - response.Result[0].Targets[0] = target.GetId() + response.Result[0].Condition = cond + response.Result[0].Targets = targets return nil }, req: &action.ListExecutionsRequest{ @@ -538,17 +613,17 @@ func TestServer_ListExecutions_Request(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - ExecutionId: "request./zitadel.management.v1.ManagementService/UpdateAction", - Targets: []string{""}, + Condition: &action.Condition{}, + Targets: executionTargetsSingleTarget(""), }, }, }, }, { - name: "list single include", + name: "list request single include", args: args{ ctx: CTX, dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - Tester.SetExecution(ctx, t, &action.Condition{ + cond := &action.Condition{ ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ @@ -556,8 +631,11 @@ func TestServer_ListExecutions_Request(t *testing.T) { }, }, }, - }, []string{targetResp.GetId()}, []string{}) - resp2 := Tester.SetExecution(ctx, t, &action.Condition{ + } + Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId())) + request.Queries[0].GetIncludeQuery().Include = cond + + includeCond := &action.Condition{ ConditionType: &action.Condition_Request{ Request: &action.RequestExecution{ Condition: &action.RequestExecution_Method{ @@ -565,19 +643,23 @@ func TestServer_ListExecutions_Request(t *testing.T) { }, }, }, - }, []string{}, []string{"request./zitadel.management.v1.ManagementService/GetAction"}) + } + includeTargets := executionTargetsSingleInclude(cond) + resp2 := Tester.SetExecution(ctx, t, includeCond, includeTargets) response.Details.Timestamp = resp2.GetDetails().GetChangeDate() response.Details.ProcessedSequence = resp2.GetDetails().GetSequence() response.Result[0].Details.ChangeDate = resp2.GetDetails().GetChangeDate() response.Result[0].Details.Sequence = resp2.GetDetails().GetSequence() + response.Result[0].Condition = includeCond + response.Result[0].Targets = includeTargets return nil }, req: &action.ListExecutionsRequest{ Queries: []*action.SearchQuery{{ Query: &action.SearchQuery_IncludeQuery{ - IncludeQuery: &action.IncludeQuery{Include: "request./zitadel.management.v1.ManagementService/GetAction"}, + IncludeQuery: &action.IncludeQuery{}, }, }}, }, @@ -591,8 +673,6 @@ func TestServer_ListExecutions_Request(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - ExecutionId: "request./zitadel.management.v1.ManagementService/ListActions", - Includes: []string{"request./zitadel.management.v1.ManagementService/GetAction"}, }, }, }, @@ -603,19 +683,32 @@ func TestServer_ListExecutions_Request(t *testing.T) { ctx: CTX, dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { - resp1 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{}) + cond1 := request.Queries[0].GetInConditionsQuery().GetConditions()[0] + targets1 := executionTargetsSingleTarget(targetResp.GetId()) + resp1 := Tester.SetExecution(ctx, t, cond1, targets1) response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate() response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence() + response.Result[0].Condition = cond1 + response.Result[0].Targets = targets1 - resp2 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[1], []string{targetResp.GetId()}, []string{}) + cond2 := request.Queries[0].GetInConditionsQuery().GetConditions()[1] + targets2 := executionTargetsSingleTarget(targetResp.GetId()) + resp2 := Tester.SetExecution(ctx, t, cond2, targets2) response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate() response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence() + response.Result[1].Condition = cond2 + response.Result[1].Targets = targets2 - resp3 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[2], []string{targetResp.GetId()}, []string{}) - response.Details.Timestamp = resp3.GetDetails().GetChangeDate() - response.Details.ProcessedSequence = resp3.GetDetails().GetSequence() + cond3 := request.Queries[0].GetInConditionsQuery().GetConditions()[2] + targets3 := executionTargetsSingleTarget(targetResp.GetId()) + resp3 := Tester.SetExecution(ctx, t, cond3, targets3) response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate() response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence() + response.Result[2].Condition = cond3 + response.Result[2].Targets = targets3 + + response.Details.Timestamp = resp3.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp3.GetDetails().GetSequence() return nil }, req: &action.ListExecutionsRequest{ @@ -665,24 +758,77 @@ func TestServer_ListExecutions_Request(t *testing.T) { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession", - Targets: []string{targetResp.GetId()}, }, { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - ExecutionId: "request./zitadel.session.v2beta.SessionService/CreateSession", - Targets: []string{targetResp.GetId()}, }, { Details: &object.Details{ ResourceOwner: Tester.Instance.InstanceID(), }, - ExecutionId: "request./zitadel.session.v2beta.SessionService/SetSession", - Targets: []string{targetResp.GetId()}, }, }, }, }, + { + name: "list multiple conditions all types", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error { + targets := executionTargetsSingleTarget(targetResp.GetId()) + for i, cond := range request.Queries[0].GetInConditionsQuery().GetConditions() { + resp := Tester.SetExecution(ctx, t, cond, targets) + response.Result[i].Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Result[i].Details.Sequence = resp.GetDetails().GetSequence() + response.Result[i].Condition = cond + response.Result[i].Targets = targets + + // filled with info of last sequence + response.Details.Timestamp = resp.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + } + + return nil + }, + req: &action.ListExecutionsRequest{ + Queries: []*action.SearchQuery{{ + Query: &action.SearchQuery_InConditionsQuery{ + InConditionsQuery: &action.InConditionsQuery{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Details: &object.ListDetails{ + TotalResult: 10, + }, + Result: []*action.Execution{ + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + {Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -699,20 +845,33 @@ func TestServer_ListExecutions_Request(t *testing.T) { require.EventuallyWithT(t, func(ttt *assert.CollectT) { got, listErr := Client.ListExecutions(tt.args.ctx, tt.args.req) if tt.wantErr { - assert.Error(ttt, listErr, "Error: "+listErr.Error()) + assert.Error(t, listErr, "Error: "+listErr.Error()) } else { - assert.NoError(ttt, listErr) + assert.NoError(t, listErr) } if listErr != nil { return } // always first check length, otherwise its failed anyway - assert.Len(ttt, got.Result, len(tt.want.Result)) + assert.Len(t, got.Result, len(tt.want.Result)) for i := range tt.want.Result { - assert.Contains(ttt, got.Result, tt.want.Result[i]) + // as not sorted, all elements have to be checked + // workaround as oneof elements can only be checked with assert.EqualExportedValues() + if j, found := containExecution(got.Result, tt.want.Result[i]); found { + assert.EqualExportedValues(t, tt.want.Result[i], got.Result[j]) + } } integration.AssertListDetails(t, tt.want, got) }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") }) } } + +func containExecution(executionList []*action.Execution, execution *action.Execution) (int, bool) { + for i, exec := range executionList { + if reflect.DeepEqual(exec.Details, execution.Details) { + return i, true + } + } + return 0, false +} diff --git a/internal/api/grpc/action/v3alpha/server.go b/internal/api/grpc/action/v3alpha/server.go index dfd813a5ad..952a555d24 100644 --- a/internal/api/grpc/action/v3alpha/server.go +++ b/internal/api/grpc/action/v3alpha/server.go @@ -66,5 +66,5 @@ func checkExecutionEnabled(ctx context.Context) error { if authz.GetInstance(ctx).Features().Actions { return nil } - return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-141bwx3lef", "Errors.action.NotEnabled") + return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled") } diff --git a/internal/api/grpc/action/v3alpha/target.go b/internal/api/grpc/action/v3alpha/target.go index 1a01ad2260..c57d5b607f 100644 --- a/internal/api/grpc/action/v3alpha/target.go +++ b/internal/api/grpc/action/v3alpha/target.go @@ -58,23 +58,26 @@ func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetReque } func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { - var targetType domain.TargetType - var url string + var ( + targetType domain.TargetType + interruptOnError bool + ) switch t := req.GetTargetType().(type) { case *action.CreateTargetRequest_RestWebhook: targetType = domain.TargetTypeWebhook - url = t.RestWebhook.GetUrl() - case *action.CreateTargetRequest_RestRequestResponse: - targetType = domain.TargetTypeRequestResponse - url = t.RestRequestResponse.GetUrl() + interruptOnError = t.RestWebhook.InterruptOnError + case *action.CreateTargetRequest_RestCall: + targetType = domain.TargetTypeCall + interruptOnError = t.RestCall.InterruptOnError + case *action.CreateTargetRequest_RestAsync: + targetType = domain.TargetTypeAsync } return &command.AddTarget{ Name: req.GetName(), TargetType: targetType, - URL: url, + Endpoint: req.GetEndpoint(), Timeout: req.GetTimeout().AsDuration(), - Async: req.GetIsAsync(), - InterruptOnError: req.GetInterruptOnError(), + InterruptOnError: interruptOnError, } } @@ -86,22 +89,24 @@ func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarge ObjectRoot: models.ObjectRoot{ AggregateID: req.GetTargetId(), }, - Name: req.Name, + Name: req.Name, + Endpoint: req.Endpoint, } - switch t := req.GetTargetType().(type) { - case *action.UpdateTargetRequest_RestWebhook: - target.TargetType = gu.Ptr(domain.TargetTypeWebhook) - target.URL = gu.Ptr(t.RestWebhook.GetUrl()) - case *action.UpdateTargetRequest_RestRequestResponse: - target.TargetType = gu.Ptr(domain.TargetTypeRequestResponse) - target.URL = gu.Ptr(t.RestRequestResponse.GetUrl()) + if req.TargetType != nil { + switch t := req.GetTargetType().(type) { + case *action.UpdateTargetRequest_RestWebhook: + target.TargetType = gu.Ptr(domain.TargetTypeWebhook) + target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError) + case *action.UpdateTargetRequest_RestCall: + target.TargetType = gu.Ptr(domain.TargetTypeCall) + target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError) + case *action.UpdateTargetRequest_RestAsync: + target.TargetType = gu.Ptr(domain.TargetTypeAsync) + target.InterruptOnError = gu.Ptr(false) + } } if req.Timeout != nil { target.Timeout = gu.Ptr(req.GetTimeout().AsDuration()) } - if req.ExecutionType != nil { - target.Async = gu.Ptr(req.GetIsAsync()) - target.InterruptOnError = gu.Ptr(req.GetInterruptOnError()) - } return target } diff --git a/internal/api/grpc/action/v3alpha/target_integration_test.go b/internal/api/grpc/action/v3alpha/target_integration_test.go index 2d45bed6d8..8b143fddb8 100644 --- a/internal/api/grpc/action/v3alpha/target_integration_test.go +++ b/internal/api/grpc/action/v3alpha/target_integration_test.go @@ -14,6 +14,7 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" @@ -69,8 +70,8 @@ func TestServer_CreateTarget(t *testing.T) { ctx: CTX, req: &action.CreateTargetRequest{ Name: fmt.Sprint(time.Now().UnixNano() + 1), - TargetType: &action.CreateTargetRequest_RestRequestResponse{ - RestRequestResponse: &action.SetRESTRequestResponse{}, + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{}, }, }, wantErr: true, @@ -79,29 +80,25 @@ func TestServer_CreateTarget(t *testing.T) { name: "empty timeout", ctx: CTX, req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, + RestWebhook: &action.SetRESTWebhook{}, }, - Timeout: nil, - ExecutionType: nil, + Timeout: nil, }, wantErr: true, }, { - name: "empty execution type, ok", + name: "async, ok", ctx: CTX, req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.SetRESTAsync{}, }, - Timeout: durationpb.New(10 * time.Second), - ExecutionType: nil, + Timeout: durationpb.New(10 * time.Second), }, want: &action.CreateTargetResponse{ Details: &object.Details{ @@ -111,19 +108,17 @@ func TestServer_CreateTarget(t *testing.T) { }, }, { - name: "async execution, ok", + name: "webhook, ok", ctx: CTX, req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", TargetType: &action.CreateTargetRequest_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + InterruptOnError: false, }, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.CreateTargetRequest_IsAsync{ - IsAsync: true, - }, }, want: &action.CreateTargetResponse{ Details: &object.Details{ @@ -133,20 +128,59 @@ func TestServer_CreateTarget(t *testing.T) { }, }, { - name: "interrupt on error execution, ok", + name: "webhook, interrupt on error, ok", ctx: CTX, req: &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", TargetType: &action.CreateTargetRequest_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", + InterruptOnError: true, }, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.CreateTargetRequest_InterruptOnError{ - InterruptOnError: true, + }, + want: &action.CreateTargetResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), }, }, + }, + { + name: "call, ok", + ctx: CTX, + req: &action.CreateTargetRequest{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: &action.CreateTargetResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + }, + }, + + { + name: "call, interruptOnError, ok", + ctx: CTX, + req: &action.CreateTargetRequest{ + Name: fmt.Sprint(time.Now().UnixNano() + 1), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, want: &action.CreateTargetResponse{ Details: &object.Details{ ChangeDate: timestamppb.Now(), @@ -186,7 +220,7 @@ func TestServer_UpdateTarget(t *testing.T) { { name: "missing permission", prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t).GetId() + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() request.TargetId = targetID return nil }, @@ -215,7 +249,7 @@ func TestServer_UpdateTarget(t *testing.T) { { name: "change name, ok", prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t).GetId() + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() request.TargetId = targetID return nil }, @@ -235,16 +269,16 @@ func TestServer_UpdateTarget(t *testing.T) { { name: "change type, ok", prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t).GetId() + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() request.TargetId = targetID return nil }, args: args{ ctx: CTX, req: &action.UpdateTargetRequest{ - TargetType: &action.UpdateTargetRequest_RestRequestResponse{ - RestRequestResponse: &action.SetRESTRequestResponse{ - Url: "https://example.com", + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, }, }, }, @@ -259,18 +293,14 @@ func TestServer_UpdateTarget(t *testing.T) { { name: "change url, ok", prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t).GetId() + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() request.TargetId = targetID return nil }, args: args{ ctx: CTX, req: &action.UpdateTargetRequest{ - TargetType: &action.UpdateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com/hooks/new", - }, - }, + Endpoint: gu.Ptr("https://example.com/hooks/new"), }, }, want: &action.UpdateTargetResponse{ @@ -283,7 +313,7 @@ func TestServer_UpdateTarget(t *testing.T) { { name: "change timeout, ok", prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t).GetId() + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() request.TargetId = targetID return nil }, @@ -301,17 +331,17 @@ func TestServer_UpdateTarget(t *testing.T) { }, }, { - name: "change execution type, ok", + name: "change type async, ok", prepare: func(request *action.UpdateTargetRequest) error { - targetID := Tester.CreateTarget(CTX, t).GetId() + targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId() request.TargetId = targetID return nil }, args: args{ ctx: CTX, req: &action.UpdateTargetRequest{ - ExecutionType: &action.UpdateTargetRequest_IsAsync{ - IsAsync: true, + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.SetRESTAsync{}, }, }, }, @@ -341,7 +371,7 @@ func TestServer_UpdateTarget(t *testing.T) { func TestServer_DeleteTarget(t *testing.T) { ensureFeatureEnabled(t) - target := Tester.CreateTarget(CTX, t) + target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) tests := []struct { name string ctx context.Context diff --git a/internal/api/grpc/action/v3alpha/target_test.go b/internal/api/grpc/action/v3alpha/target_test.go index f630a6c715..23e33ad9be 100644 --- a/internal/api/grpc/action/v3alpha/target_test.go +++ b/internal/api/grpc/action/v3alpha/target_test.go @@ -27,55 +27,64 @@ func Test_createTargetToCommand(t *testing.T) { args: args{nil}, want: &command.AddTarget{ Name: "", - URL: "", + Endpoint: "", Timeout: 0, - Async: false, InterruptOnError: false, }, }, { - name: "all fields (async webhook)", + name: "all fields (webhook)", args: args{&action.CreateTargetRequest{ - Name: "target 1", + Name: "target 1", + Endpoint: "https://example.com/hooks/1", TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com/hooks/1", - }, + RestWebhook: &action.SetRESTWebhook{}, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.CreateTargetRequest_IsAsync{ - IsAsync: true, - }, }}, want: &command.AddTarget{ Name: "target 1", TargetType: domain.TargetTypeWebhook, - URL: "https://example.com/hooks/1", + Endpoint: "https://example.com/hooks/1", + Timeout: 10 * time.Second, + InterruptOnError: false, + }, + }, + { + name: "all fields (async)", + args: args{&action.CreateTargetRequest{ + Name: "target 1", + Endpoint: "https://example.com/hooks/1", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.SetRESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.AddTarget{ + Name: "target 1", + TargetType: domain.TargetTypeAsync, + Endpoint: "https://example.com/hooks/1", Timeout: 10 * time.Second, - Async: true, InterruptOnError: false, }, }, { name: "all fields (interrupting response)", args: args{&action.CreateTargetRequest{ - Name: "target 1", - TargetType: &action.CreateTargetRequest_RestRequestResponse{ - RestRequestResponse: &action.SetRESTRequestResponse{ - Url: "https://example.com/hooks/1", + Name: "target 1", + Endpoint: "https://example.com/hooks/1", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, }, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.CreateTargetRequest_InterruptOnError{ - InterruptOnError: true, - }, }}, want: &command.AddTarget{ Name: "target 1", - TargetType: domain.TargetTypeRequestResponse, - URL: "https://example.com/hooks/1", + TargetType: domain.TargetTypeCall, + Endpoint: "https://example.com/hooks/1", Timeout: 10 * time.Second, - Async: false, InterruptOnError: true, }, }, @@ -105,80 +114,108 @@ func Test_updateTargetToCommand(t *testing.T) { { name: "all fields nil", args: args{&action.UpdateTargetRequest{ - Name: nil, - TargetType: nil, - Timeout: nil, - ExecutionType: nil, + Name: nil, + TargetType: nil, + Timeout: nil, }}, want: &command.ChangeTarget{ Name: nil, TargetType: nil, - URL: nil, + Endpoint: nil, Timeout: nil, - Async: nil, InterruptOnError: nil, }, }, { name: "all fields empty", args: args{&action.UpdateTargetRequest{ - Name: gu.Ptr(""), - TargetType: nil, - Timeout: durationpb.New(0), - ExecutionType: nil, + Name: gu.Ptr(""), + TargetType: nil, + Timeout: durationpb.New(0), }}, want: &command.ChangeTarget{ Name: gu.Ptr(""), TargetType: nil, - URL: nil, + Endpoint: nil, Timeout: gu.Ptr(0 * time.Second), - Async: nil, InterruptOnError: nil, }, }, { - name: "all fields (async webhook)", + name: "all fields (webhook)", args: args{&action.UpdateTargetRequest{ - Name: gu.Ptr("target 1"), + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), TargetType: &action.UpdateTargetRequest_RestWebhook{ RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com/hooks/1", + InterruptOnError: false, }, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.UpdateTargetRequest_IsAsync{ - IsAsync: true, - }, }}, want: &command.ChangeTarget{ Name: gu.Ptr("target 1"), TargetType: gu.Ptr(domain.TargetTypeWebhook), - URL: gu.Ptr("https://example.com/hooks/1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + Timeout: gu.Ptr(10 * time.Second), + InterruptOnError: gu.Ptr(false), + }, + }, + { + name: "all fields (webhook interrupt)", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr("target 1"), + TargetType: gu.Ptr(domain.TargetTypeWebhook), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + Timeout: gu.Ptr(10 * time.Second), + InterruptOnError: gu.Ptr(true), + }, + }, + { + name: "all fields (async)", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.SetRESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr("target 1"), + TargetType: gu.Ptr(domain.TargetTypeAsync), + Endpoint: gu.Ptr("https://example.com/hooks/1"), Timeout: gu.Ptr(10 * time.Second), - Async: gu.Ptr(true), InterruptOnError: gu.Ptr(false), }, }, { name: "all fields (interrupting response)", args: args{&action.UpdateTargetRequest{ - Name: gu.Ptr("target 1"), - TargetType: &action.UpdateTargetRequest_RestRequestResponse{ - RestRequestResponse: &action.SetRESTRequestResponse{ - Url: "https://example.com/hooks/1", + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: true, }, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &action.UpdateTargetRequest_InterruptOnError{ - InterruptOnError: true, - }, }}, want: &command.ChangeTarget{ Name: gu.Ptr("target 1"), - TargetType: gu.Ptr(domain.TargetTypeRequestResponse), - URL: gu.Ptr("https://example.com/hooks/1"), + TargetType: gu.Ptr(domain.TargetTypeCall), + Endpoint: gu.Ptr("https://example.com/hooks/1"), Timeout: gu.Ptr(10 * time.Second), - Async: gu.Ptr(false), InterruptOnError: gu.Ptr(true), }, }, diff --git a/internal/api/grpc/auth/multi_factor.go b/internal/api/grpc/auth/multi_factor.go index b7c23da1ad..ed3cf421d5 100644 --- a/internal/api/grpc/auth/multi_factor.go +++ b/internal/api/grpc/auth/multi_factor.go @@ -120,7 +120,7 @@ func (s *Server) RemoveMyAuthFactorOTPEmail(ctx context.Context, _ *auth_pb.Remo func (s *Server) AddMyAuthFactorU2F(ctx context.Context, _ *auth_pb.AddMyAuthFactorU2FRequest) (*auth_pb.AddMyAuthFactorU2FResponse, error) { ctxData := authz.GetCtxData(ctx) - u2f, err := s.command.HumanAddU2FSetup(ctx, ctxData.UserID, ctxData.ResourceOwner, false) + u2f, err := s.command.HumanAddU2FSetup(ctx, ctxData.UserID, ctxData.ResourceOwner) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/passwordless.go b/internal/api/grpc/auth/passwordless.go index 3b0aa5357d..b7b914d2c2 100644 --- a/internal/api/grpc/auth/passwordless.go +++ b/internal/api/grpc/auth/passwordless.go @@ -41,7 +41,7 @@ func (s *Server) ListMyPasswordless(ctx context.Context, _ *auth_pb.ListMyPasswo func (s *Server) AddMyPasswordless(ctx context.Context, _ *auth_pb.AddMyPasswordlessRequest) (*auth_pb.AddMyPasswordlessResponse, error) { ctxData := authz.GetCtxData(ctx) - token, err := s.command.HumanAddPasswordlessSetup(ctx, ctxData.UserID, ctxData.ResourceOwner, false, domain.AuthenticatorAttachmentUnspecified) + token, err := s.command.HumanAddPasswordlessSetup(ctx, ctxData.UserID, ctxData.ResourceOwner, domain.AuthenticatorAttachmentUnspecified) if err != nil { return nil, err } diff --git a/internal/api/grpc/org/v2/org_integration_test.go b/internal/api/grpc/org/v2/org_integration_test.go index d369cbeea7..7276e8d5eb 100644 --- a/internal/api/grpc/org/v2/org_integration_test.go +++ b/internal/api/grpc/org/v2/org_integration_test.go @@ -41,7 +41,7 @@ func TestMain(m *testing.M) { } func TestServer_AddOrganization(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) tests := []struct { name string diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go new file mode 100644 index 0000000000..ec4eee17d2 --- /dev/null +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -0,0 +1,179 @@ +package middleware + +import ( + "context" + "encoding/json" + "strings" + + "github.com/zitadel/logging" + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/execution" + "github.com/zitadel/zitadel/internal/query" + exec_repo "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + requestTargets, responseTargets := queryTargets(ctx, queries, info.FullMethod) + + // call targets otherwise return req + handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req) + if err != nil { + return nil, err + } + + response, err := handler(ctx, handledReq) + if err != nil { + return nil, err + } + + return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response) + } +} + +func executeTargetsForRequest(ctx context.Context, targets []execution.Target, fullMethod string, req interface{}) (_ interface{}, err error) { + ctx, span := tracing.NewSpan(ctx) + defer span.EndWithError(err) + + // if no targets are found, return without any calls + if len(targets) == 0 { + return req, nil + } + + ctxData := authz.GetCtxData(ctx) + info := &ContextInfoRequest{ + FullMethod: fullMethod, + InstanceID: authz.GetInstance(ctx).InstanceID(), + ProjectID: ctxData.ProjectID, + OrgID: ctxData.OrgID, + UserID: ctxData.UserID, + Request: req, + } + + return execution.CallTargets(ctx, targets, info) +} + +func executeTargetsForResponse(ctx context.Context, targets []execution.Target, fullMethod string, req, resp interface{}) (_ interface{}, err error) { + ctx, span := tracing.NewSpan(ctx) + defer span.EndWithError(err) + + // if no targets are found, return without any calls + if len(targets) == 0 { + return resp, nil + } + + ctxData := authz.GetCtxData(ctx) + info := &ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: authz.GetInstance(ctx).InstanceID(), + ProjectID: ctxData.ProjectID, + OrgID: ctxData.OrgID, + UserID: ctxData.UserID, + Request: req, + Response: resp, + } + + return execution.CallTargets(ctx, targets, info) +} + +type ExecutionQueries interface { + TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error) +} + +func queryTargets( + ctx context.Context, + queries ExecutionQueries, + fullMethod string, +) ([]execution.Target, []execution.Target) { + ctx, span := tracing.NewSpan(ctx) + defer span.End() + + targets, err := queries.TargetsByExecutionIDs(ctx, + idsForFullMethod(fullMethod, domain.ExecutionTypeRequest), + idsForFullMethod(fullMethod, domain.ExecutionTypeResponse), + ) + requestTargets := make([]execution.Target, 0, len(targets)) + responseTargets := make([]execution.Target, 0, len(targets)) + if err != nil { + logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets") + return requestTargets, responseTargets + } + + for _, target := range targets { + if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeRequest)) { + requestTargets = append(requestTargets, target) + } else if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeResponse)) { + responseTargets = append(responseTargets, target) + } + } + + return requestTargets, responseTargets +} + +func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string { + return []string{exec_repo.ID(executionType, fullMethod), exec_repo.ID(executionType, serviceFromFullMethod(fullMethod)), exec_repo.IDAll(executionType)} +} + +func serviceFromFullMethod(s string) string { + parts := strings.Split(s, "/") + return parts[1] +} + +var _ execution.ContextInfo = &ContextInfoRequest{} + +type ContextInfoRequest struct { + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request interface{} `json:"request,omitempty"` +} + +func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error { + return json.Unmarshal(resp, c.Request) +} + +func (c *ContextInfoRequest) GetContent() interface{} { + return c.Request +} + +var _ execution.ContextInfo = &ContextInfoResponse{} + +type ContextInfoResponse struct { + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request interface{} `json:"request,omitempty"` + Response interface{} `json:"response,omitempty"` +} + +func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error { + return json.Unmarshal(resp, c.Response) +} + +func (c *ContextInfoResponse) GetContent() interface{} { + return c.Response +} diff --git a/internal/api/grpc/server/middleware/execution_interceptor_test.go b/internal/api/grpc/server/middleware/execution_interceptor_test.go new file mode 100644 index 0000000000..bbc87c374f --- /dev/null +++ b/internal/api/grpc/server/middleware/execution_interceptor_test.go @@ -0,0 +1,778 @@ +package middleware + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/execution" +) + +var _ execution.Target = &mockExecutionTarget{} + +type mockExecutionTarget struct { + InstanceID string + ExecutionID string + TargetID string + TargetType domain.TargetType + Endpoint string + Timeout time.Duration + InterruptOnError bool +} + +func (e *mockExecutionTarget) SetEndpoint(endpoint string) { + e.Endpoint = endpoint +} +func (e *mockExecutionTarget) IsInterruptOnError() bool { + return e.InterruptOnError +} +func (e *mockExecutionTarget) GetEndpoint() string { + return e.Endpoint +} +func (e *mockExecutionTarget) GetTargetType() domain.TargetType { + return e.TargetType +} +func (e *mockExecutionTarget) GetTimeout() time.Duration { + return e.Timeout +} +func (e *mockExecutionTarget) GetTargetID() string { + return e.TargetID +} +func (e *mockExecutionTarget) GetExecutionID() string { + return e.ExecutionID +} + +type mockContentRequest struct { + Content string +} + +func newMockContentRequest(content string) *mockContentRequest { + return &mockContentRequest{ + Content: content, + } +} + +func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest { + return &ContextInfoRequest{ + FullMethod: fullMethod, + Request: newMockContentRequest(request), + } +} + +func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse { + return &ContextInfoResponse{ + FullMethod: fullMethod, + Request: newMockContentRequest(request), + Response: newMockContentRequest(response), + } +} + +func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { + type target struct { + reqBody execution.ContextInfo + sleep time.Duration + statusCode int + respBody interface{} + } + type args struct { + ctx context.Context + + executionTargets []execution.Target + targets []target + fullMethod string + req interface{} + } + type res struct { + want interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "target, executionTargets nil", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: nil, + req: newMockContentRequest("request"), + }, + res{ + want: newMockContentRequest("request"), + }, + }, + { + "target, executionTargets empty", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{}, + req: newMockContentRequest("request"), + }, + res{ + want: newMockContentRequest("request"), + }, + }, + { + "target, not reachable", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{}, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, error without interrupt", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusBadRequest, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "target, interruptOnError", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusBadRequest, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, wrong request", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + }, + }, + targets: []target{ + {reqBody: newMockContextInfoRequest("/service/method", "wrong")}, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content1"), + }, + }, + { + "target async, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeAsync, + Timeout: time.Second, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "target async, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeAsync, + Timeout: time.Minute, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "webhook, error", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeWebhook, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + sleep: 0, + statusCode: http.StatusInternalServerError, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "webhook, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeWebhook, + Timeout: time.Second, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "webhook, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeWebhook, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "with includes, interruptOnError", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target1", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target2", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target3", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content1"), + respBody: newMockContentRequest("content2"), + sleep: 0, + statusCode: http.StatusBadRequest, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content2"), + respBody: newMockContentRequest("content3"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "with includes, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target1", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target2", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target3", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content1"), + respBody: newMockContentRequest("content2"), + sleep: 5 * time.Second, + statusCode: http.StatusBadRequest, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content2"), + respBody: newMockContentRequest("content3"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeFuncs := make([]func(), len(tt.args.targets)) + for i, target := range tt.args.targets { + url, closeF := testServerCall( + target.reqBody, + target.sleep, + target.statusCode, + target.respBody, + ) + + et := tt.args.executionTargets[i].(*mockExecutionTarget) + et.SetEndpoint(url) + closeFuncs[i] = closeF + } + + resp, err := executeTargetsForRequest( + tt.args.ctx, + tt.args.executionTargets, + tt.args.fullMethod, + tt.args.req, + ) + + if tt.res.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.res.want, resp) + + for _, closeF := range closeFuncs { + closeF() + } + }) + } +} + +func testServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody interface{}, +) (string, func()) { + handler := func(w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error", http.StatusInternalServerError) + return + } + + if statusCode != http.StatusOK { + http.Error(w, "error", statusCode) + return + } + + time.Sleep(sleep) + + w.Header().Set("Content-Type", "application/json") + resp, err := json.Marshal(respBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := io.WriteString(w, string(resp)); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + + return server.URL, server.Close +} + +func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { + type target struct { + reqBody execution.ContextInfo + sleep time.Duration + statusCode int + respBody interface{} + } + type args struct { + ctx context.Context + + executionTargets []execution.Target + targets []target + fullMethod string + req interface{} + resp interface{} + } + type res struct { + want interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "target, executionTargets nil", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: nil, + req: newMockContentRequest("request"), + resp: newMockContentRequest("response"), + }, + res{ + want: newMockContentRequest("response"), + }, + }, + { + "target, executionTargets empty", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{}, + req: newMockContentRequest("request"), + resp: newMockContentRequest("response"), + }, + res{ + want: newMockContentRequest("response"), + }, + }, + { + "target, empty response", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentRequest(""), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: []byte{}, + }, + res{ + wantErr: true, + }, + }, + { + "target, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoResponse("/service/method", "request", "response"), + respBody: newMockContentRequest("response1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("request"), + resp: newMockContentRequest("response"), + }, + res{ + want: newMockContentRequest("response1"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeFuncs := make([]func(), len(tt.args.targets)) + for i, target := range tt.args.targets { + url, closeF := testServerCall( + target.reqBody, + target.sleep, + target.statusCode, + target.respBody, + ) + + et := tt.args.executionTargets[i].(*mockExecutionTarget) + et.SetEndpoint(url) + closeFuncs[i] = closeF + } + + resp, err := executeTargetsForResponse( + tt.args.ctx, + tt.args.executionTargets, + tt.args.fullMethod, + tt.args.req, + tt.args.resp, + ) + + if tt.res.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.res.want, resp) + + for _, closeF := range closeFuncs { + closeF() + } + }) + } +} diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 4f0a6140bc..ef4c271bf5 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -58,6 +58,7 @@ func CreateServer( middleware.AuthorizationInterceptor(verifier, authConfig), middleware.TranslationHandler(), middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), + middleware.ExecutionHandler(queries), middleware.ValidationHandler(), middleware.ServiceHandler(), middleware.ActivityInterceptor(), diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index cc70c75ea7..6818b627e8 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -361,8 +361,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) { } func TestServer_CreateSession_successfulIntent(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) - + idpID := Tester.AddGenericOAuthProvider(t, CTX) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ @@ -375,7 +374,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0) - intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), SessionToken: createResp.GetSessionToken(), @@ -391,9 +390,9 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { } func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) - intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), "id") createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ User: &session.CheckUser{ @@ -412,7 +411,7 @@ func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { } func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -427,7 +426,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0) idpUserID := "id" - intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID) + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", idpUserID) updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), SessionToken: createResp.GetSessionToken(), @@ -440,6 +439,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }) require.Error(t, err) Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) + intentID, token, _, _ = Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, User.GetUserId(), idpUserID) updateResp, err = Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), SessionToken: createResp.GetSessionToken(), @@ -455,7 +455,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { } func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -469,7 +469,7 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0) - intentID := Tester.CreateIntent(t, idpID) + intentID := Tester.CreateIntent(t, CTX, idpID) _, err = Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), SessionToken: createResp.GetSessionToken(), diff --git a/internal/api/grpc/user/v2/otp_integration_test.go b/internal/api/grpc/user/v2/otp_integration_test.go index 7471675c7e..7f4c4a0f43 100644 --- a/internal/api/grpc/user/v2/otp_integration_test.go +++ b/internal/api/grpc/user/v2/otp_integration_test.go @@ -19,12 +19,26 @@ func TestServer_AddOTPSMS(t *testing.T) { Tester.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) - // TODO: add when phone can be added to user - /* - userIDPhone := Tester.CreateHumanUser(CTX).GetUserId() - Tester.RegisterUserPasskey(CTX, userIDPhone) - _, sessionTokenPhone, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userIDPhone) - */ + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + + userVerified := Tester.CreateHumanUser(CTX) + _, err := Tester.Client.UserV2.VerifyPhone(CTX, &user.VerifyPhoneRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetPhoneCode(), + }) + require.NoError(t, err) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + + userVerified2 := Tester.CreateHumanUser(CTX) + _, err = Tester.Client.UserV2.VerifyPhone(CTX, &user.VerifyPhoneRequest{ + UserId: userVerified2.GetUserId(), + VerificationCode: userVerified2.GetPhoneCode(), + }) + require.NoError(t, err) + type args struct { ctx context.Context req *user.AddOTPSMSRequest @@ -46,9 +60,9 @@ func TestServer_AddOTPSMS(t *testing.T) { { name: "user mismatch", args: args{ - ctx: CTX, + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser), req: &user.AddOTPSMSRequest{ - UserId: "wrong", + UserId: userID, }, }, wantErr: true, @@ -63,23 +77,34 @@ func TestServer_AddOTPSMS(t *testing.T) { }, wantErr: true, }, - // TODO: add when phone can be added to user - /* - { - name: "add success", - args: args{ - ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenPhone), - req: &user.AddOTPSMSRequest{ - UserId: userID, - }, - }, - want: &user.AddOTPSMSResponse{ - Details: &object.Details{ - ResourceOwner: Tester.Organisation.ID, - }, + { + name: "add success", + args: args{ + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified), + req: &user.AddOTPSMSRequest{ + UserId: userVerified.GetUserId(), }, }, - */ + want: &user.AddOTPSMSResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "add success, admin", + args: args{ + ctx: CTX, + req: &user.AddOTPSMSRequest{ + UserId: userVerified2.GetUserId(), + }, + }, + want: &user.AddOTPSMSResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -96,12 +121,21 @@ func TestServer_AddOTPSMS(t *testing.T) { } func TestServer_RemoveOTPSMS(t *testing.T) { - // TODO: add when phone can be added to user - /* - userID := Tester.CreateHumanUser(CTX).GetUserId() - Tester.RegisterUserPasskey(CTX, userID) - _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) - */ + userID := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + + userVerified := Tester.CreateHumanUser(CTX) + Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified) + _, err := Tester.Client.UserV2.VerifyPhone(userVerifiedCtx, &user.VerifyPhoneRequest{ + UserId: userVerified.GetUserId(), + VerificationCode: userVerified.GetPhoneCode(), + }) + require.NoError(t, err) + _, err = Tester.Client.UserV2.AddOTPSMS(userVerifiedCtx, &user.AddOTPSMSRequest{UserId: userVerified.GetUserId()}) + require.NoError(t, err) type args struct { ctx context.Context @@ -116,30 +150,27 @@ func TestServer_RemoveOTPSMS(t *testing.T) { { name: "not added", args: args{ - ctx: CTX, + ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), req: &user.RemoveOTPSMSRequest{ - UserId: "wrong", + UserId: userID, }, }, wantErr: true, }, - // TODO: add when phone can be added to user - /* - { - name: "success", - args: args{ - ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken), - req: &user.RemoveOTPSMSRequest{ - UserId: userID, - }, - }, - want: &user.RemoveOTPSMSResponse{ - Details: &object.Details{ - ResourceOwner: Tester.Organisation.ResourceOwner, - }, + { + name: "success", + args: args{ + ctx: userVerifiedCtx, + req: &user.RemoveOTPSMSRequest{ + UserId: userVerified.GetUserId(), }, }, - */ + want: &user.RemoveOTPSMSResponse{ + Details: &object.Details{ + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -160,6 +191,10 @@ func TestServer_AddOTPEmail(t *testing.T) { Tester.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + userVerified := Tester.CreateHumanUser(CTX) _, err := Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{ UserId: userVerified.GetUserId(), @@ -169,6 +204,13 @@ func TestServer_AddOTPEmail(t *testing.T) { Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + userVerified2 := Tester.CreateHumanUser(CTX) + _, err = Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{ + UserId: userVerified2.GetUserId(), + VerificationCode: userVerified2.GetEmailCode(), + }) + require.NoError(t, err) + type args struct { ctx context.Context req *user.AddOTPEmailRequest @@ -190,9 +232,9 @@ func TestServer_AddOTPEmail(t *testing.T) { { name: "user mismatch", args: args{ - ctx: CTX, + ctx: Tester.WithAuthorizationToken(context.Background(), sessionTokenOtherUser), req: &user.AddOTPEmailRequest{ - UserId: "wrong", + UserId: userID, }, }, wantErr: true, @@ -222,6 +264,21 @@ func TestServer_AddOTPEmail(t *testing.T) { }, }, }, + { + name: "add success, admin", + args: args{ + ctx: CTX, + req: &user.AddOTPEmailRequest{ + UserId: userVerified2.GetUserId(), + }, + }, + want: &user.AddOTPEmailResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/totp_integration_test.go b/internal/api/grpc/user/v2/totp_integration_test.go index 086a281af9..cf7e6ffa3a 100644 --- a/internal/api/grpc/user/v2/totp_integration_test.go +++ b/internal/api/grpc/user/v2/totp_integration_test.go @@ -23,6 +23,11 @@ func TestServer_RegisterTOTP(t *testing.T) { _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) ctx := Tester.WithAuthorizationToken(CTX, sessionToken) + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + ctxOtherUser := Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser) + type args struct { ctx context.Context req *user.RegisterTOTPRequest @@ -44,13 +49,28 @@ func TestServer_RegisterTOTP(t *testing.T) { { name: "user mismatch", args: args{ - ctx: ctx, + ctx: ctxOtherUser, req: &user.RegisterTOTPRequest{ - UserId: "wrong", + UserId: userID, }, }, wantErr: true, }, + { + name: "admin", + args: args{ + ctx: CTX, + req: &user.RegisterTOTPRequest{ + UserId: userID, + }, + }, + want: &user.RegisterTOTPResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, { name: "success", args: args{ @@ -96,6 +116,18 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { code, err := totp.GenerateCode(reg.Secret, time.Now()) require.NoError(t, err) + otherUser := Tester.CreateHumanUser(CTX).GetUserId() + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) + ctxOtherUser := Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser) + + regOtherUser, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{ + UserId: otherUser, + }) + require.NoError(t, err) + codeOtherUser, err := totp.GenerateCode(regOtherUser.Secret, time.Now()) + require.NoError(t, err) + type args struct { ctx context.Context req *user.VerifyTOTPRegistrationRequest @@ -109,9 +141,9 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { { name: "user mismatch", args: args{ - ctx: ctx, + ctx: ctxOtherUser, req: &user.VerifyTOTPRegistrationRequest{ - UserId: "wrong", + UserId: userID, }, }, wantErr: true, @@ -143,6 +175,22 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) { }, }, }, + { + name: "success, admin", + args: args{ + ctx: CTX, + req: &user.VerifyTOTPRegistrationRequest{ + UserId: otherUser, + Code: codeOtherUser, + }, + }, + want: &user.VerifyTOTPRegistrationResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ResourceOwner, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2/u2f_integration_test.go b/internal/api/grpc/user/v2/u2f_integration_test.go index 78c620e3c2..3b7fbd293c 100644 --- a/internal/api/grpc/user/v2/u2f_integration_test.go +++ b/internal/api/grpc/user/v2/u2f_integration_test.go @@ -18,10 +18,13 @@ import ( func TestServer_RegisterU2F(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() + otherUser := Tester.CreateHumanUser(CTX).GetUserId() // We also need a user session Tester.RegisterUserPasskey(CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) + Tester.RegisterUserPasskey(CTX, otherUser) + _, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser) type args struct { ctx context.Context @@ -42,13 +45,28 @@ func TestServer_RegisterU2F(t *testing.T) { wantErr: true, }, { - name: "user mismatch", + name: "admin user", args: args{ ctx: CTX, req: &user.RegisterU2FRequest{ UserId: userID, }, }, + want: &user.RegisterU2FResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "other user, no permission", + args: args{ + ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser), + req: &user.RegisterU2FRequest{ + UserId: userID, + }, + }, wantErr: true, }, { diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index d08df3d8f9..6ff420a44b 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -370,7 +370,7 @@ func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.Star } func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetCtxData(ctx).OrgID) + intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } @@ -394,7 +394,7 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re } func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { - intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetCtxData(ctx).OrgID) + intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } @@ -473,7 +473,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), authz.GetCtxData(ctx).OrgID) + intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") if err != nil { return nil, err } @@ -481,7 +481,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R return nil, err } if intent.State != domain.IDPIntentStateSucceeded { - return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Hk38e", "Errors.Intent.NotSucceeded") + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } return idpIntentToIDPIntentPb(intent, s.idpAlg) } diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index 8c070caab0..132ed0b30f 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -54,7 +54,7 @@ func TestMain(m *testing.M) { } func TestServer_AddHumanUser(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) type args struct { ctx context.Context req *user.AddHumanUserRequest @@ -1752,7 +1752,7 @@ func TestServer_DeleteUser(t *testing.T) { } func TestServer_AddIDPLink(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) type args struct { ctx context.Context req *user.AddIDPLinkRequest @@ -1832,10 +1832,13 @@ func TestServer_AddIDPLink(t *testing.T) { } func TestServer_StartIdentityProviderIntent(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) - samlIdpID := Tester.AddSAMLProvider(t) - samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) - samlPostIdpID := Tester.AddSAMLPostProvider(t) + idpID := Tester.AddGenericOAuthProvider(t, CTX) + orgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, Tester.Organisation.ID) + orgResp := Tester.CreateOrganization(IamCTX, fmt.Sprintf("NotDefaultOrg%d", time.Now().UnixNano()), fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())) + notDefaultOrgIdpID := Tester.AddOrgGenericOAuthProvider(t, CTX, orgResp.OrganizationId) + samlIdpID := Tester.AddSAMLProvider(t, CTX) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX) + samlPostIdpID := Tester.AddSAMLPostProvider(t, CTX) type args struct { ctx context.Context req *user.StartIdentityProviderIntentRequest @@ -1880,7 +1883,100 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { want: want{ details: &object.Details{ ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step oauth auth url, default org", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: orgIdpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step oauth auth url, default org", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: notDefaultOrgIdpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), + }, + url: "https://example.com/oauth/v2/authorize", + parametersEqual: map[string]string{ + "client_id": "clientID", + "prompt": "select_account", + "redirect_uri": "http://" + Tester.Config.ExternalDomain + ":8080/idps/callback", + "response_type": "code", + "scope": "openid profile email", + }, + parametersExisting: []string{"state"}, + }, + wantErr: false, + }, + { + name: "next step oauth auth url org", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: orgIdpID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Instance.InstanceID(), }, url: "https://example.com/oauth/v2/authorize", parametersEqual: map[string]string{ @@ -1911,7 +2007,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { want: want{ details: &object.Details{ ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, @@ -1935,7 +2031,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { want: want{ details: &object.Details{ ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, url: "http://" + Tester.Config.ExternalDomain + ":8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, @@ -1959,7 +2055,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { want: want{ details: &object.Details{ ChangeDate: timestamppb.Now(), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), }, postForm: true, }, @@ -1999,13 +2095,13 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Tester.AddGenericOAuthProvider(t) - intentID := Tester.CreateIntent(t, idpID) - successfulID, token, changeDate, sequence := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", "id") - successfulWithUserID, WithUsertoken, WithUserchangeDate, WithUsersequence := Tester.CreateSuccessfulOAuthIntent(t, idpID, "user", "id") - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "", "id") - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "user", "id") - samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Tester.CreateSuccessfulSAMLIntent(t, idpID, "", "id") + idpID := Tester.AddGenericOAuthProvider(t, CTX) + intentID := Tester.CreateIntent(t, CTX, idpID) + successfulID, token, changeDate, sequence := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "", "id") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence := Tester.CreateSuccessfulOAuthIntent(t, CTX, idpID, "user", "id") + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Tester.CreateSuccessfulLDAPIntent(t, CTX, idpID, "", "id") + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence := Tester.CreateSuccessfulLDAPIntent(t, CTX, idpID, "user", "id") + samlSuccessfulID, samlToken, samlChangeDate, samlSequence := Tester.CreateSuccessfulSAMLIntent(t, CTX, idpID, "", "id") type args struct { ctx context.Context req *user.RetrieveIdentityProviderIntentRequest @@ -2050,7 +2146,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { want: &user.RetrieveIdentityProviderIntentResponse{ Details: &object.Details{ ChangeDate: timestamppb.New(changeDate), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), Sequence: sequence, }, IdpInformation: &user.IDPInformation{ @@ -2081,14 +2177,14 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { CTX, &user.RetrieveIdentityProviderIntentRequest{ IdpIntentId: successfulWithUserID, - IdpIntentToken: WithUsertoken, + IdpIntentToken: withUsertoken, }, }, want: &user.RetrieveIdentityProviderIntentResponse{ Details: &object.Details{ - ChangeDate: timestamppb.New(WithUserchangeDate), - ResourceOwner: Tester.Organisation.ID, - Sequence: WithUsersequence, + ChangeDate: timestamppb.New(withUserchangeDate), + ResourceOwner: Tester.Instance.InstanceID(), + Sequence: withUsersequence, }, UserId: "user", IdpInformation: &user.IDPInformation{ @@ -2125,7 +2221,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { want: &user.RetrieveIdentityProviderIntentResponse{ Details: &object.Details{ ChangeDate: timestamppb.New(ldapChangeDate), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), Sequence: ldapSequence, }, IdpInformation: &user.IDPInformation{ @@ -2170,7 +2266,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { want: &user.RetrieveIdentityProviderIntentResponse{ Details: &object.Details{ ChangeDate: timestamppb.New(ldapWithUserChangeDate), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), Sequence: ldapWithUserSequence, }, UserId: "user", @@ -2216,7 +2312,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { want: &user.RetrieveIdentityProviderIntentResponse{ Details: &object.Details{ ChangeDate: timestamppb.New(samlChangeDate), - ResourceOwner: Tester.Organisation.ID, + ResourceOwner: Tester.Instance.InstanceID(), Sequence: samlSequence, }, IdpInformation: &user.IDPInformation{ diff --git a/internal/api/idp/idp_integration_test.go b/internal/api/idp/idp_integration_test.go index 8ad76a475d..1ade5d7f3e 100644 --- a/internal/api/idp/idp_integration_test.go +++ b/internal/api/idp/idp_integration_test.go @@ -52,8 +52,8 @@ func TestMain(m *testing.M) { } func TestServer_SAMLCertificate(t *testing.T) { - samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) - oauthIdpID := Tester.AddGenericOAuthProvider(t) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX) + oauthIdpID := Tester.AddGenericOAuthProvider(t, CTX) type args struct { ctx context.Context @@ -109,8 +109,8 @@ func TestServer_SAMLCertificate(t *testing.T) { } func TestServer_SAMLMetadata(t *testing.T) { - samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) - oauthIdpID := Tester.AddGenericOAuthProvider(t) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX) + oauthIdpID := Tester.AddGenericOAuthProvider(t, CTX) type args struct { ctx context.Context @@ -167,7 +167,7 @@ func TestServer_SAMLMetadata(t *testing.T) { func TestServer_SAMLACS(t *testing.T) { userHuman := Tester.CreateHumanUser(CTX) - samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t) + samlRedirectIdpID := Tester.AddSAMLRedirectProvider(t, CTX) externalUserID := "test1" linkedExternalUserID := "test2" Tester.CreateUserIDPlink(CTX, userHuman.UserId, linkedExternalUserID, samlRedirectIdpID, linkedExternalUserID) diff --git a/internal/api/ui/login/login_handler.go b/internal/api/ui/login/login_handler.go index 31f16b8555..ae21d84d87 100644 --- a/internal/api/ui/login/login_handler.go +++ b/internal/api/ui/login/login_handler.go @@ -95,7 +95,7 @@ func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *dom if err != nil { errID, errMessage = l.getErrorMessage(r, err) } - if singleIDPAllowed(authReq) { + if err == nil && singleIDPAllowed(authReq) { l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) return } diff --git a/internal/api/ui/login/mfa_init_u2f.go b/internal/api/ui/login/mfa_init_u2f.go index 2cd1029ee5..0b7718bb80 100644 --- a/internal/api/ui/login/mfa_init_u2f.go +++ b/internal/api/ui/login/mfa_init_u2f.go @@ -21,7 +21,7 @@ func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authRe var errID, errMessage, credentialData string var u2f *domain.WebAuthNToken if err == nil { - u2f, err = l.command.HumanAddU2FSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, true) + u2f, err = l.command.HumanAddU2FSetup(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID) } if err != nil { errID, errMessage = l.getErrorMessage(r, err) @@ -54,7 +54,7 @@ func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) { } userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - if _, err = l.command.HumanVerifyU2FSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, data.Name, userAgentID, credData); err != nil { + if _, err = l.command.HumanVerifyU2FSetup(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, data.Name, userAgentID, credData); err != nil { l.renderRegisterU2F(w, r, authReq, err) return } diff --git a/internal/api/ui/login/mfa_init_verify_handler.go b/internal/api/ui/login/mfa_init_verify_handler.go index d8e930b19b..0b66451434 100644 --- a/internal/api/ui/login/mfa_init_verify_handler.go +++ b/internal/api/ui/login/mfa_init_verify_handler.go @@ -50,7 +50,7 @@ func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) { func (l *Login) handleOTPVerify(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData { userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - _, err := l.command.HumanCheckMFATOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID) + _, err := l.command.HumanCheckMFATOTPSetup(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID) if err == nil { return nil } diff --git a/internal/api/ui/login/mfa_prompt_handler.go b/internal/api/ui/login/mfa_prompt_handler.go index bee8df2160..ade1b53229 100644 --- a/internal/api/ui/login/mfa_prompt_handler.go +++ b/internal/api/ui/login/mfa_prompt_handler.go @@ -96,7 +96,7 @@ func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authRe } func (l *Login) handleTOTPCreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) { - otp, err := l.command.AddHumanTOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID) + otp, err := l.command.AddHumanTOTP(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID) if err != nil { l.renderError(w, r, authReq, err) return diff --git a/internal/api/ui/login/passwordless_registration_handler.go b/internal/api/ui/login/passwordless_registration_handler.go index 4c2d379b48..0346374cee 100644 --- a/internal/api/ui/login/passwordless_registration_handler.go +++ b/internal/api/ui/login/passwordless_registration_handler.go @@ -87,9 +87,9 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re var webAuthNToken *domain.WebAuthNToken if err == nil { if authReq != nil { - webAuthNToken, err = l.authRepo.BeginPasswordlessSetup(setContext(r.Context(), authReq.UserOrgID), userID, authReq.UserOrgID, domain.AuthenticatorAttachment(requestedPlatformType)) + webAuthNToken, err = l.authRepo.BeginPasswordlessSetup(setUserContext(r.Context(), userID, authReq.UserOrgID), userID, authReq.UserOrgID, domain.AuthenticatorAttachment(requestedPlatformType)) } else { - webAuthNToken, err = l.authRepo.BeginPasswordlessInitCodeSetup(setContext(r.Context(), orgID), userID, orgID, codeID, code, domain.AuthenticatorAttachment(requestedPlatformType)) + webAuthNToken, err = l.authRepo.BeginPasswordlessInitCodeSetup(setUserContext(r.Context(), userID, orgID), userID, orgID, codeID, code, domain.AuthenticatorAttachment(requestedPlatformType)) } } if err != nil { diff --git a/internal/api/ui/login/static/templates/login.html b/internal/api/ui/login/static/templates/login.html index 72837c7f97..7c063d9912 100644 --- a/internal/api/ui/login/static/templates/login.html +++ b/internal/api/ui/login/static/templates/login.html @@ -36,7 +36,9 @@ + {{if hasUsernamePasswordLogin}} + {{end}} {{if hasRegistration}} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index efdb20505a..129369a71f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -447,7 +447,7 @@ func (repo *AuthRequestRepo) VerifyMFAU2F(ctx context.Context, userID, resourceO func (repo *AuthRequestRepo) BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string, authenticatorPlatform domain.AuthenticatorAttachment) (login *domain.WebAuthNToken, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return repo.Command.HumanAddPasswordlessSetup(ctx, userID, resourceOwner, true, authenticatorPlatform) + return repo.Command.HumanAddPasswordlessSetup(ctx, userID, resourceOwner, authenticatorPlatform) } func (repo *AuthRequestRepo) VerifyPasswordlessSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName string, credentialData []byte) (err error) { diff --git a/internal/command/action_v2_execution.go b/internal/command/action_v2_execution.go index 164830d6e5..422e053daf 100644 --- a/internal/command/action_v2_execution.go +++ b/internal/command/action_v2_execution.go @@ -2,6 +2,7 @@ package command import ( "context" + "strings" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -9,6 +10,10 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +const ( + EventGroupSuffix = ".*" +) + type ExecutionAPICondition struct { Method string Service string @@ -134,7 +139,11 @@ func (e *ExecutionEventCondition) ID() string { return execution.ID(domain.ExecutionTypeEvent, e.Event) } if e.Group != "" { - return execution.ID(domain.ExecutionTypeEvent, e.Group) + group := e.Group + if !strings.HasSuffix(e.Group, EventGroupSuffix) { + group += EventGroupSuffix + } + return execution.ID(domain.ExecutionTypeEvent, group) } if e.All { return execution.IDAll(domain.ExecutionTypeEvent) @@ -168,25 +177,43 @@ func (c *Commands) SetExecutionEvent(ctx context.Context, cond *ExecutionEventCo type SetExecution struct { models.ObjectRoot - Targets []string - Includes []string + Targets []*execution.Target +} + +func (t SetExecution) GetIncludes() []string { + includes := make([]string, 0) + for i := range t.Targets { + if t.Targets[i].Type == domain.ExecutionTargetTypeInclude { + includes = append(includes, t.Targets[i].Target) + } + } + return includes +} + +func (t SetExecution) GetTargets() []string { + targets := make([]string, 0) + for i := range t.Targets { + if t.Targets[i].Type == domain.ExecutionTargetTypeTarget { + targets = append(targets, t.Targets[i].Target) + } + } + return targets } func (e *SetExecution) IsValid() error { - if len(e.Targets) == 0 && len(e.Includes) == 0 { + if len(e.Targets) == 0 { return zerrors.ThrowInvalidArgument(nil, "COMMAND-56bteot2uj", "Errors.Execution.NoTargets") } - if len(e.Targets) > 0 && len(e.Includes) > 0 { - return zerrors.ThrowInvalidArgument(nil, "COMMAND-5zleae34r1", "Errors.Execution.Invalid") - } return nil } func (e *SetExecution) Existing(c *Commands, ctx context.Context, resourceOwner string) error { - if len(e.Targets) > 0 && !c.existsTargetsByIDs(ctx, e.Targets, resourceOwner) { + targets := e.GetTargets() + if len(targets) > 0 && !c.existsTargetsByIDs(ctx, targets, resourceOwner) { return zerrors.ThrowNotFound(nil, "COMMAND-17e8fq1ggk", "Errors.Target.NotFound") } - if len(e.Includes) > 0 && !c.existsExecutionsByIDs(ctx, e.Includes, resourceOwner) { + includes := e.GetIncludes() + if len(includes) > 0 && !c.existsExecutionsByIDs(ctx, includes, resourceOwner) { return zerrors.ThrowNotFound(nil, "COMMAND-slgj0l4cdz", "Errors.Execution.IncludeNotFound") } return nil @@ -206,11 +233,10 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource return nil, err } - if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEvent( + if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEventV2( ctx, ExecutionAggregateFromWriteModel(&wm.WriteModel), set.Targets, - set.Includes, )); err != nil { return nil, err } diff --git a/internal/command/action_v2_execution_model.go b/internal/command/action_v2_execution_model.go index 0dbeb3d874..c53992856e 100644 --- a/internal/command/action_v2_execution_model.go +++ b/internal/command/action_v2_execution_model.go @@ -10,12 +10,13 @@ import ( type ExecutionWriteModel struct { eventstore.WriteModel - Targets []string - Includes []string + Targets []string + Includes []string + ExecutionTargets []*execution.Target } func (e *ExecutionWriteModel) Exists() bool { - return len(e.Targets) > 0 || len(e.Includes) > 0 + return len(e.ExecutionTargets) > 0 || len(e.Includes) > 0 || len(e.Targets) > 0 } func NewExecutionWriteModel(id string, resourceOwner string) *ExecutionWriteModel { @@ -34,9 +35,12 @@ func (wm *ExecutionWriteModel) Reduce() error { case *execution.SetEvent: wm.Targets = e.Targets wm.Includes = e.Includes + case *execution.SetEventV2: + wm.ExecutionTargets = e.Targets case *execution.RemovedEvent: wm.Targets = nil wm.Includes = nil + wm.ExecutionTargets = nil } } return wm.WriteModel.Reduce() @@ -49,6 +53,7 @@ func (wm *ExecutionWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateTypes(execution.AggregateType). AggregateIDs(wm.AggregateID). EventTypes(execution.SetEventType, + execution.SetEventV2Type, execution.RemovedEventType). Builder() } @@ -91,6 +96,10 @@ func (wm *ExecutionsExistWriteModel) Reduce() error { if !slices.Contains(wm.existingIDs, e.Aggregate().ID) { wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID) } + case *execution.SetEventV2: + if !slices.Contains(wm.existingIDs, e.Aggregate().ID) { + wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID) + } case *execution.RemovedEvent: i := slices.Index(wm.existingIDs, e.Aggregate().ID) if i >= 0 { @@ -108,6 +117,7 @@ func (wm *ExecutionsExistWriteModel) Query() *eventstore.SearchQueryBuilder { AggregateTypes(execution.AggregateType). AggregateIDs(wm.ids...). EventTypes(execution.SetEventType, + execution.SetEventV2Type, execution.RemovedEventType). Builder() } diff --git a/internal/command/action_v2_execution_model_test.go b/internal/command/action_v2_execution_model_test.go index 4c74a146e8..1c0f535d09 100644 --- a/internal/command/action_v2_execution_model_test.go +++ b/internal/command/action_v2_execution_model_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/execution" ) @@ -32,10 +33,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -53,10 +56,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -65,10 +70,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -91,10 +98,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -112,10 +121,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -138,24 +149,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -174,17 +191,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -193,10 +214,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -214,17 +237,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -233,10 +260,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -254,24 +283,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -299,24 +334,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), ), @@ -334,10 +375,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -346,17 +389,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -385,10 +432,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution1", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( @@ -397,17 +446,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) { ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution2", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( - execution.NewSetEvent(context.Background(), + execution.NewSetEventV2(context.Background(), execution.NewAggregate("execution3", "org1"), - []string{"target"}, - []string{"include"}, + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, ), ), eventFromEventPusher( diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index 6a40eadf5e..c8f91f49b2 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -19,10 +19,9 @@ func existsMock(exists bool) func(method string) bool { return exists } } - func TestCommands_SetExecutionRequest(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore grpcMethodExists func(method string) bool grpcServiceExists func(method string) bool } @@ -45,7 +44,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -60,13 +59,13 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -75,7 +74,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -85,7 +84,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -94,7 +93,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -105,7 +104,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -114,7 +113,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -125,83 +124,16 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, 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), + eventstore: expectEventstore(), grpcMethodExists: existsMock(false), }, args{ @@ -212,9 +144,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -223,7 +157,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "service not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcServiceExists: existsMock(false), }, args{ @@ -234,9 +168,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -245,25 +181,25 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -277,38 +213,40 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -322,38 +260,40 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -366,20 +306,22 @@ func TestCommands_SetExecutionRequest(t *testing.T) { true, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push not found, method include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), grpcMethodExists: existsMock(true), @@ -392,9 +334,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -403,21 +347,23 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, method include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.include", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/include", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), - nil, - []string{"request.include"}, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, ), ), ), @@ -431,20 +377,22 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push not found, service include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), grpcServiceExists: existsMock(true), @@ -457,9 +405,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -468,21 +418,23 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, service include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.include", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/include", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), - nil, - []string{"request.include"}, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, ), ), ), @@ -496,20 +448,22 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push not found, all include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -521,9 +475,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { true, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -532,21 +488,23 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, all include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.include", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/include", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request", "org1"), - nil, - []string{"request.include"}, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, ), ), ), @@ -559,13 +517,15 @@ func TestCommands_SetExecutionRequest(t *testing.T) { true, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -573,7 +533,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), GrpcMethodExisting: tt.fields.grpcMethodExists, GrpcServiceExisting: tt.fields.grpcServiceExists, } @@ -593,7 +553,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { func TestCommands_SetExecutionResponse(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore grpcMethodExists func(method string) bool grpcServiceExists func(method string) bool } @@ -616,7 +576,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -631,13 +591,13 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -646,7 +606,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -656,7 +616,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -665,7 +625,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -676,7 +636,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -685,7 +645,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -696,30 +656,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { 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", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -728,24 +665,24 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "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, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -759,9 +696,11 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -770,7 +709,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "method not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(false), }, args{ @@ -781,9 +720,11 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -792,7 +733,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "service not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcServiceExists: existsMock(false), }, args{ @@ -803,9 +744,11 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -814,25 +757,25 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -846,38 +789,31 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -891,38 +827,31 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -935,13 +864,15 @@ func TestCommands_SetExecutionResponse(t *testing.T) { true, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -949,7 +880,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), GrpcMethodExisting: tt.fields.grpcMethodExists, GrpcServiceExisting: tt.fields.grpcServiceExists, } @@ -969,7 +900,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { func TestCommands_SetExecutionEvent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore eventExists func(string) bool eventGroupExists func(string) bool } @@ -992,7 +923,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1007,13 +938,13 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionEventCondition{}, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1022,7 +953,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1032,7 +963,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1041,7 +972,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventExists: existsMock(true), }, args{ @@ -1052,7 +983,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1061,7 +992,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventExists: existsMock(true), }, args{ @@ -1072,30 +1003,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { 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", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1104,26 +1012,17 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1137,9 +1036,11 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1148,7 +1049,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "event not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventExists: existsMock(false), }, args{ @@ -1159,9 +1060,11 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1170,7 +1073,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "group not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventGroupExists: existsMock(false), }, args{ @@ -1181,9 +1084,11 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1192,25 +1097,16 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "push ok, event target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.event", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1224,38 +1120,31 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, group target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.group", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/group.*", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1269,38 +1158,31 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1313,13 +1195,15 @@ func TestCommands_SetExecutionEvent(t *testing.T) { true, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -1327,7 +1211,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), EventExisting: tt.fields.eventExists, EventGroupExisting: tt.fields.eventGroupExists, } @@ -1347,7 +1231,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { func TestCommands_SetExecutionFunction(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore actionFunctionExists func(string) bool } type args struct { @@ -1369,7 +1253,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(true), }, args{ @@ -1385,13 +1269,13 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: "", set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1400,14 +1284,14 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(true), }, args{ ctx: context.Background(), cond: "function", set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1416,33 +1300,14 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), 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", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1451,26 +1316,17 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1480,9 +1336,11 @@ func TestCommands_SetExecutionFunction(t *testing.T) { ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1490,7 +1348,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { }, { "push error, function target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), actionFunctionExists: existsMock(true), @@ -1499,9 +1357,11 @@ func TestCommands_SetExecutionFunction(t *testing.T) { ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1510,16 +1370,18 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "push error, function not existing", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(false), }, args{ ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1528,25 +1390,16 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "push ok, function target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1556,13 +1409,15 @@ func TestCommands_SetExecutionFunction(t *testing.T) { ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -1570,7 +1425,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), ActionFunctionExisting: tt.fields.actionFunctionExists, } details, err := c.SetExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.set, tt.args.resourceOwner) @@ -1589,7 +1444,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { func TestCommands_DeleteExecutionRequest(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1609,7 +1464,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1623,12 +1478,12 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1637,7 +1492,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1646,7 +1501,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "notvalid", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1655,20 +1510,21 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request.valid", "org1"), + execution.NewAggregate("request/valid", "instance"), ), ), ), @@ -1680,7 +1536,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1689,7 +1545,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "not found, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1700,7 +1556,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1709,19 +1565,20 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), + execution.NewAggregate("request/method", "instance"), ), ), ), @@ -1733,30 +1590,31 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), + execution.NewAggregate("request/service", "instance"), ), ), ), @@ -1768,30 +1626,31 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "service", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request", "org1"), + execution.NewAggregate("request", "instance"), ), ), ), @@ -1803,11 +1662,11 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -1815,7 +1674,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } details, err := c.DeleteExecutionRequest(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { @@ -1833,7 +1692,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { func TestCommands_DeleteExecutionResponse(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1853,7 +1712,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1867,12 +1726,12 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1881,7 +1740,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1890,7 +1749,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "notvalid", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1899,20 +1758,21 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response.valid", "org1"), + execution.NewAggregate("response/valid", "instance"), ), ), ), @@ -1924,7 +1784,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1933,7 +1793,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "not found, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1944,7 +1804,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1953,19 +1813,20 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response.method", "org1"), + execution.NewAggregate("response/method", "instance"), ), ), ), @@ -1977,30 +1838,31 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response.service", "org1"), + execution.NewAggregate("response/service", "instance"), ), ), ), @@ -2012,30 +1874,31 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "service", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response", "org1"), + execution.NewAggregate("response", "instance"), ), ), ), @@ -2047,11 +1910,11 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -2059,7 +1922,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } details, err := c.DeleteExecutionResponse(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { @@ -2077,7 +1940,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { func TestCommands_DeleteExecutionEvent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -2097,7 +1960,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -2111,12 +1974,12 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionEventCondition{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -2125,20 +1988,21 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), + execution.NewAggregate("event/valid", "instance"), ), ), ), @@ -2150,7 +2014,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -2159,7 +2023,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push error, not existing", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2170,7 +2034,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2179,7 +2043,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push error, event", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2190,7 +2054,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2199,19 +2063,20 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push ok, event", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), + execution.NewAggregate("event/valid", "instance"), ), ), ), @@ -2223,18 +2088,18 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push error, group", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2245,7 +2110,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "valid", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2254,19 +2119,20 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push ok, group", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.group", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/group", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event.group", "org1"), + execution.NewAggregate("event/group.*", "instance"), ), ), ), @@ -2278,18 +2144,18 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "group", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push error, all", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2300,7 +2166,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2309,19 +2175,20 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push ok, all", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event", "org1"), + execution.NewAggregate("event", "instance"), ), ), ), @@ -2333,11 +2200,11 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -2345,7 +2212,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } details, err := c.DeleteExecutionEvent(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { @@ -2363,7 +2230,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { func TestCommands_DeleteExecutionFunction(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -2383,7 +2250,7 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -2397,12 +2264,12 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: "", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -2411,20 +2278,21 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), + execution.NewAggregate("function/function", "instance"), ), ), ), @@ -2432,7 +2300,7 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { args{ ctx: context.Background(), cond: "function", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -2441,14 +2309,14 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "push error, not existing", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, args{ ctx: context.Background(), cond: "function", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2457,19 +2325,20 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "push ok, function", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), + execution.NewAggregate("function/function", "instance"), ), ), ), @@ -2477,11 +2346,11 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { args{ ctx: context.Background(), cond: "function", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -2489,7 +2358,7 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } details, err := c.DeleteExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/action_v2_target.go b/internal/command/action_v2_target.go index 1bd2a62070..913bfb2299 100644 --- a/internal/command/action_v2_target.go +++ b/internal/command/action_v2_target.go @@ -16,9 +16,8 @@ type AddTarget struct { Name string TargetType domain.TargetType - URL string + Endpoint string Timeout time.Duration - Async bool InterruptOnError bool } @@ -29,9 +28,9 @@ func (a *AddTarget) IsValid() error { if a.Timeout == 0 { return zerrors.ThrowInvalidArgument(nil, "COMMAND-39f35d8uri", "Errors.Target.NoTimeout") } - _, err := url.Parse(a.URL) - if err != nil || a.URL == "" { - return zerrors.ThrowInvalidArgument(nil, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL") + _, err := url.Parse(a.Endpoint) + if err != nil || a.Endpoint == "" { + return zerrors.ThrowInvalidArgument(err, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL") } return nil @@ -65,9 +64,8 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner TargetAggregateFromWriteModel(&wm.WriteModel), add.Name, add.TargetType, - add.URL, + add.Endpoint, add.Timeout, - add.Async, add.InterruptOnError, )) if err != nil { @@ -84,9 +82,8 @@ type ChangeTarget struct { Name *string TargetType *domain.TargetType - URL *string + Endpoint *string Timeout *time.Duration - Async *bool InterruptOnError *bool } @@ -100,10 +97,10 @@ func (a *ChangeTarget) IsValid() error { if a.Timeout != nil && *a.Timeout == 0 { return zerrors.ThrowInvalidArgument(nil, "COMMAND-08b39vdi57", "Errors.Target.NoTimeout") } - if a.URL != nil { - _, err := url.Parse(*a.URL) - if err != nil || *a.URL == "" { - return zerrors.ThrowInvalidArgument(nil, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL") + if a.Endpoint != nil { + _, err := url.Parse(*a.Endpoint) + if err != nil || *a.Endpoint == "" { + return zerrors.ThrowInvalidArgument(err, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL") } } return nil @@ -130,9 +127,8 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou TargetAggregateFromWriteModel(&existing.WriteModel), change.Name, change.TargetType, - change.URL, + change.Endpoint, change.Timeout, - change.Async, change.InterruptOnError) if changedEvent == nil { return writeModelToObjectDetails(&existing.WriteModel), nil diff --git a/internal/command/action_v2_target_model.go b/internal/command/action_v2_target_model.go index 60f70c40e0..24dd76c80a 100644 --- a/internal/command/action_v2_target_model.go +++ b/internal/command/action_v2_target_model.go @@ -15,9 +15,8 @@ type TargetWriteModel struct { Name string TargetType domain.TargetType - URL string + Endpoint string Timeout time.Duration - Async bool InterruptOnError bool State domain.TargetState @@ -39,9 +38,8 @@ func (wm *TargetWriteModel) Reduce() error { case *target.AddedEvent: wm.Name = e.Name wm.TargetType = e.TargetType - wm.URL = e.URL + wm.Endpoint = e.Endpoint wm.Timeout = e.Timeout - wm.Async = e.Async wm.State = domain.TargetActive case *target.ChangedEvent: if e.Name != nil { @@ -50,15 +48,12 @@ func (wm *TargetWriteModel) Reduce() error { if e.TargetType != nil { wm.TargetType = *e.TargetType } - if e.URL != nil { - wm.URL = *e.URL + if e.Endpoint != nil { + wm.Endpoint = *e.Endpoint } if e.Timeout != nil { wm.Timeout = *e.Timeout } - if e.Async != nil { - wm.Async = *e.Async - } if e.InterruptOnError != nil { wm.InterruptOnError = *e.InterruptOnError } @@ -86,9 +81,8 @@ func (wm *TargetWriteModel) NewChangedEvent( agg *eventstore.Aggregate, name *string, targetType *domain.TargetType, - url *string, + endpoint *string, timeout *time.Duration, - async *bool, interruptOnError *bool, ) *target.ChangedEvent { changes := make([]target.Changes, 0) @@ -98,15 +92,12 @@ func (wm *TargetWriteModel) NewChangedEvent( if targetType != nil && wm.TargetType != *targetType { changes = append(changes, target.ChangeTargetType(*targetType)) } - if url != nil && wm.URL != *url { - changes = append(changes, target.ChangeURL(*url)) + if endpoint != nil && wm.Endpoint != *endpoint { + changes = append(changes, target.ChangeEndpoint(*endpoint)) } if timeout != nil && wm.Timeout != *timeout { changes = append(changes, target.ChangeTimeout(*timeout)) } - if async != nil && wm.Async != *async { - changes = append(changes, target.ChangeAsync(*async)) - } if interruptOnError != nil && wm.InterruptOnError != *interruptOnError { changes = append(changes, target.ChangeInterruptOnError(*interruptOnError)) } diff --git a/internal/command/action_v2_target_model_test.go b/internal/command/action_v2_target_model_test.go index 4c0f31c8e0..8042da23b1 100644 --- a/internal/command/action_v2_target_model_test.go +++ b/internal/command/action_v2_target_model_test.go @@ -20,7 +20,6 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent { "https://example.com", time.Second, false, - false, ) } diff --git a/internal/command/action_v2_target_test.go b/internal/command/action_v2_target_test.go index 8600ffabf1..ef60baae49 100644 --- a/internal/command/action_v2_target_test.go +++ b/internal/command/action_v2_target_test.go @@ -19,7 +19,7 @@ import ( func TestCommands_AddTarget(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator } type args struct { @@ -41,7 +41,7 @@ func TestCommands_AddTarget(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -55,12 +55,12 @@ func TestCommands_AddTarget(t *testing.T) { { "no name, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), add: &AddTarget{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -69,50 +69,50 @@ func TestCommands_AddTarget(t *testing.T) { { "no timeout, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), add: &AddTarget{ Name: "name", }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, }, }, { - "no url, error", + "no Endpoint, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), add: &AddTarget{ - Name: "name", - Timeout: time.Second, - URL: "", + Name: "name", + Timeout: time.Second, + Endpoint: "", }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, }, }, { - "no parsable url, error", + "no parsable Endpoint, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), add: &AddTarget{ - Name: "name", - Timeout: time.Second, - URL: "://", + Name: "name", + Timeout: time.Second, + Endpoint: "://", }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -121,18 +121,17 @@ func TestCommands_AddTarget(t *testing.T) { { "unique constraint failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), + target.NewAggregate("id1", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, false, - false, ), ), ), @@ -142,11 +141,11 @@ func TestCommands_AddTarget(t *testing.T) { ctx: context.Background(), add: &AddTarget{ Name: "name", - URL: "https://example.com", + Endpoint: "https://example.com", Timeout: time.Second, TargetType: domain.TargetTypeWebhook, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -155,16 +154,10 @@ func TestCommands_AddTarget(t *testing.T) { { "already existing", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - false, - false, + eventFromEventPusher( + targetAddEvent("target", "instance"), ), ), ), @@ -176,9 +169,9 @@ func TestCommands_AddTarget(t *testing.T) { Name: "name", TargetType: domain.TargetTypeWebhook, Timeout: time.Second, - URL: "https://example.com", + Endpoint: "https://example.com", }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorAlreadyExists, @@ -187,18 +180,10 @@ func TestCommands_AddTarget(t *testing.T) { { "push ok", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), expectPush( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - false, - false, - ), + targetAddEvent("id1", "instance"), ), ), idGenerator: mock.ExpectID(t, "id1"), @@ -209,32 +194,28 @@ func TestCommands_AddTarget(t *testing.T) { Name: "name", TargetType: domain.TargetTypeWebhook, Timeout: time.Second, - URL: "https://example.com", + Endpoint: "https://example.com", }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ id: "id1", details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push full ok", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), expectPush( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), + func() eventstore.Command { + event := targetAddEvent("id1", "instance") + event.InterruptOnError = true + return event + }(), ), ), idGenerator: mock.ExpectID(t, "id1"), @@ -244,17 +225,16 @@ func TestCommands_AddTarget(t *testing.T) { add: &AddTarget{ Name: "name", TargetType: domain.TargetTypeWebhook, - URL: "https://example.com", + Endpoint: "https://example.com", Timeout: time.Second, - Async: true, InterruptOnError: true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ id: "id1", details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -262,7 +242,7 @@ func TestCommands_AddTarget(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner) @@ -282,7 +262,7 @@ func TestCommands_AddTarget(t *testing.T) { func TestCommands_ChangeTarget(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -302,7 +282,7 @@ func TestCommands_ChangeTarget(t *testing.T) { { "resourceowner missing, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -316,12 +296,12 @@ func TestCommands_ChangeTarget(t *testing.T) { { "id missing, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), change: &ChangeTarget{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -330,14 +310,14 @@ func TestCommands_ChangeTarget(t *testing.T) { { "name empty, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), change: &ChangeTarget{ Name: gu.Ptr(""), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -346,46 +326,46 @@ func TestCommands_ChangeTarget(t *testing.T) { { "timeout empty, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), change: &ChangeTarget{ Timeout: gu.Ptr(time.Duration(0)), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, }, }, { - "url empty, error", + "Endpoint empty, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), change: &ChangeTarget{ - URL: gu.Ptr(""), + Endpoint: gu.Ptr(""), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, }, }, { - "url not parsable, error", + "Endpoint not parsable, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), change: &ChangeTarget{ - URL: gu.Ptr("://"), + Endpoint: gu.Ptr("://"), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -394,7 +374,7 @@ func TestCommands_ChangeTarget(t *testing.T) { { "not found, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -406,7 +386,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, Name: gu.Ptr("name"), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -415,18 +395,10 @@ func TestCommands_ChangeTarget(t *testing.T) { { "no changes", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - 0, - false, - false, - ), + targetAddEvent("target", "instance"), ), ), ), @@ -439,35 +411,27 @@ func TestCommands_ChangeTarget(t *testing.T) { }, TargetType: gu.Ptr(domain.TargetTypeWebhook), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "unique constraint failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - 0, - false, - false, - ), + targetAddEvent("target", "instance"), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), target.NewChangedEvent(context.Background(), - target.NewAggregate("id1", "org1"), + target.NewAggregate("id1", "instance"), []target.Changes{ target.ChangeName("name", "name2"), }, @@ -483,7 +447,7 @@ func TestCommands_ChangeTarget(t *testing.T) { }, Name: gu.Ptr("name2"), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -492,23 +456,15 @@ func TestCommands_ChangeTarget(t *testing.T) { { "push ok", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - 0, - false, - false, - ), + targetAddEvent("id1", "instance"), ), ), expectPush( target.NewChangedEvent(context.Background(), - target.NewAggregate("id1", "org1"), + target.NewAggregate("id1", "instance"), []target.Changes{ target.ChangeName("name", "name2"), }, @@ -524,40 +480,31 @@ func TestCommands_ChangeTarget(t *testing.T) { }, Name: gu.Ptr("name2"), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push full ok", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - 0, - false, - false, - ), + targetAddEvent("id1", "instance"), ), ), expectPush( target.NewChangedEvent(context.Background(), - target.NewAggregate("id1", "org1"), + target.NewAggregate("id1", "instance"), []target.Changes{ target.ChangeName("name", "name2"), - target.ChangeURL("https://example2.com"), - target.ChangeTargetType(domain.TargetTypeRequestResponse), - target.ChangeTimeout(time.Second), - target.ChangeAsync(true), + target.ChangeEndpoint("https://example2.com"), + target.ChangeTargetType(domain.TargetTypeCall), + target.ChangeTimeout(10 * time.Second), target.ChangeInterruptOnError(true), }, ), @@ -571,17 +518,16 @@ func TestCommands_ChangeTarget(t *testing.T) { AggregateID: "id1", }, Name: gu.Ptr("name2"), - URL: gu.Ptr("https://example2.com"), - TargetType: gu.Ptr(domain.TargetTypeRequestResponse), - Timeout: gu.Ptr(time.Second), - Async: gu.Ptr(true), + Endpoint: gu.Ptr("https://example2.com"), + TargetType: gu.Ptr(domain.TargetTypeCall), + Timeout: gu.Ptr(10 * time.Second), InterruptOnError: gu.Ptr(true), }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -589,7 +535,7 @@ func TestCommands_ChangeTarget(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner) if tt.res.err == nil { @@ -607,7 +553,7 @@ func TestCommands_ChangeTarget(t *testing.T) { func TestCommands_DeleteTarget(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -627,12 +573,12 @@ func TestCommands_DeleteTarget(t *testing.T) { { "id missing, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), id: "", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -641,14 +587,14 @@ func TestCommands_DeleteTarget(t *testing.T) { { "not found, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, args{ ctx: context.Background(), id: "id1", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -657,23 +603,15 @@ func TestCommands_DeleteTarget(t *testing.T) { { "remove ok", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("id1", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - 0, - false, - false, - ), + targetAddEvent("id1", "instance"), ), ), expectPush( target.NewRemovedEvent(context.Background(), - target.NewAggregate("id1", "org1"), + target.NewAggregate("id1", "instance"), "name", ), ), @@ -682,11 +620,11 @@ func TestCommands_DeleteTarget(t *testing.T) { args{ ctx: context.Background(), id: "id1", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -694,7 +632,7 @@ func TestCommands_DeleteTarget(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/idp.go b/internal/command/idp.go index 997a772786..a8b041c335 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/zerrors" @@ -129,7 +128,8 @@ type AppleProvider struct { IDPOptions idp.Options } -func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) { +// ExistsIDPOnOrgOrInstance query first org level IDPs and then instance level IDPs, no check if the IDP is active +func ExistsIDPOnOrgOrInstance(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, orgID, id string) (exists bool, err error) { writeModel := NewOrgIDPRemoveWriteModel(orgID, id) events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -144,7 +144,7 @@ func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, return writeModel.State.Exists(), nil } - instanceWriteModel := NewInstanceIDPRemoveWriteModel(authz.GetInstance(ctx).InstanceID(), id) + instanceWriteModel := NewInstanceIDPRemoveWriteModel(instanceID, id) events, err = filter(ctx, instanceWriteModel.Query()) if err != nil { return false, err @@ -160,6 +160,23 @@ func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, return instanceWriteModel.State.Exists(), nil } +// ExistsIDP query IDPs only with the ID, no check if the IDP is active +func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (exists bool, err error) { + writeModel := NewIDPTypeWriteModel(id) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return false, err + } + if len(events) == 0 { + return false, nil + } + writeModel.AppendEvents(events...) + if err := writeModel.Reduce(); err != nil { + return false, err + } + return writeModel.State.Exists(), nil +} + func IDPProviderWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (_ *AllIDPWriteModel, err error) { writeModel := NewIDPTypeWriteModel(id) events, err := filter(ctx, writeModel.Query()) diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 38fc9c91b4..e79977dc52 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -25,7 +25,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID string, successURL, failureURL string) preparation.Validation { +func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { if idpID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing") @@ -43,12 +43,17 @@ func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID st if err != nil { return nil, err } - exists, err := ExistsIDP(ctx, filter, idpID, writeModel.ResourceOwner) + exists, err := ExistsIDP(ctx, filter, idpID) if !exists || err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting") } return []eventstore.Command{ - idpintent.NewStartedEvent(ctx, writeModel.aggregate, successURL, failureURL, idpID), + idpintent.NewStartedEvent(ctx, + IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), + successURL, + failureURL, + idpID, + ), }, nil }, nil } @@ -64,6 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureU return nil, nil, err } + //nolint: staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL)) if err != nil { return nil, nil, err @@ -117,7 +123,7 @@ func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIn return nil, err } if intent.State == domain.IDPIntentStateUnspecified { - return nil, zerrors.ThrowNotFound(nil, "IDP-Hk38e", "Errors.Intent.NotStarted") + return nil, zerrors.ThrowNotFound(nil, "IDP-gy3ctgkqe7", "Errors.Intent.NotStarted") } if intent.State != domain.IDPIntentStateStarted { return nil, zerrors.ThrowInvalidArgument(nil, "IDP-Sfrgs", "Errors.Intent.NotStarted") @@ -166,7 +172,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr } cmd := idpintent.NewSucceededEvent( ctx, - &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), idpInfo, idpUser.GetID(), idpUser.GetPreferredUsername(), @@ -200,7 +206,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte } cmd := idpintent.NewSAMLSucceededEvent( ctx, - &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), idpInfo, idpUser.GetID(), idpUser.GetPreferredUsername(), @@ -217,7 +223,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte func (c *Commands) RequestSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, requestID string) error { return c.pushAppendAndReduce(ctx, writeModel, idpintent.NewSAMLRequestEvent( ctx, - &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), requestID, )) } @@ -241,7 +247,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte } cmd := idpintent.NewLDAPSucceededEvent( ctx, - &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), idpInfo, idpUser.GetID(), idpUser.GetPreferredUsername(), @@ -258,7 +264,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, reason string) error { cmd := idpintent.NewFailedEvent( ctx, - &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), reason, ) _, err := c.eventstore.Push(ctx, cmd) diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index b2242c3832..62794323e1 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -28,8 +28,7 @@ type IDPIntentWriteModel struct { RequestID string Assertion *crypto.CryptoValue - State domain.IDPIntentState - aggregate *eventstore.Aggregate + State domain.IDPIntentState } func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel { @@ -38,7 +37,6 @@ func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel { AggregateID: id, ResourceOwner: resourceOwner, }, - aggregate: &idpintent.NewAggregate(id, resourceOwner).Aggregate, } } @@ -121,3 +119,13 @@ func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEv func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) { wm.State = domain.IDPIntentStateFailed } + +func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { + return &eventstore.Aggregate{ + Type: idpintent.AggregateType, + Version: idpintent.AggregateVersion, + ID: wm.AggregateID, + ResourceOwner: wm.ResourceOwner, + InstanceID: wm.InstanceID, + } +} diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 3043c7848a..8bdbcb4716 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -28,20 +28,21 @@ import ( rep_idp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommands_CreateIntent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator } type args struct { - ctx context.Context - idpID string - successURL string - failureURL string - resourceOwner string + ctx context.Context + idpID string + successURL string + failureURL string + instanceID string } type res struct { intentID string @@ -57,11 +58,11 @@ func TestCommands_CreateIntent(t *testing.T) { { "error no id generator", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: mock.NewIDGeneratorExpectError(t, zerrors.ThrowInternal(nil, "", "error id")), }, args{ - ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + ctx: context.Background(), idpID: "", successURL: "https://success.url", failureURL: "https://failure.url", @@ -73,11 +74,11 @@ func TestCommands_CreateIntent(t *testing.T) { { "error no idpID", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: mock.ExpectID(t, "id"), }, args{ - ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + ctx: context.Background(), idpID: "", successURL: "https://success.url", failureURL: "https://failure.url", @@ -89,11 +90,11 @@ func TestCommands_CreateIntent(t *testing.T) { { "error no successURL", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: mock.ExpectID(t, "id"), }, args{ - ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + ctx: context.Background(), idpID: "idp", successURL: ":", failureURL: "https://failure.url", @@ -105,11 +106,11 @@ func TestCommands_CreateIntent(t *testing.T) { { "error no failureURL", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), idGenerator: mock.ExpectID(t, "id"), }, args{ - ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + ctx: context.Background(), idpID: "idp", successURL: "https://success.url", failureURL: ":", @@ -119,18 +120,18 @@ func TestCommands_CreateIntent(t *testing.T) { }, }, { - "error idp not existing", + "error idp not existing org", fields{ - eventstore: eventstoreExpect(t, - expectFilter(), + eventstore: expectEventstore( expectFilter(), expectFilter(), ), idGenerator: mock.ExpectID(t, "id"), }, args{ - ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + ctx: context.Background(), idpID: "idp", + instanceID: "instance", successURL: "https://success.url", failureURL: "https://failure.url", }, @@ -139,14 +140,33 @@ func TestCommands_CreateIntent(t *testing.T) { }, }, { - "push", + "error idp not existing instance", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), expectFilter(), + ), + idGenerator: mock.ExpectID(t, "id"), + }, + args{ + ctx: context.Background(), + idpID: "idp", + instanceID: "instance", + successURL: "https://success.url", + failureURL: "https://failure.url", + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting"), + }, + }, + { + "push, org", + fields{ + eventstore: expectEventstore( + expectFilter(), expectFilter( eventFromEventPusher( - instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("ro").Aggregate, + org.NewOAuthIDPAddedEvent(context.Background(), &org.NewAggregate("org").Aggregate, "idp", "name", "clientID", @@ -170,7 +190,7 @@ func TestCommands_CreateIntent(t *testing.T) { failure, _ := url.Parse("https://failure.url") return idpintent.NewStartedEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, success, failure, "idp", @@ -181,25 +201,131 @@ func TestCommands_CreateIntent(t *testing.T) { idGenerator: mock.ExpectID(t, "id"), }, args{ - ctx: context.Background(), - resourceOwner: "ro", - idpID: "idp", - successURL: "https://success.url", - failureURL: "https://failure.url", + ctx: context.Background(), + instanceID: "instance", + idpID: "idp", + successURL: "https://success.url", + failureURL: "https://failure.url", }, res{ intentID: "id", - details: &domain.ObjectDetails{ResourceOwner: "ro"}, + details: &domain.ObjectDetails{ResourceOwner: "instance"}, + }, + }, + { + "push, instance", + fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + "idAttribute", + nil, + rep_idp.Options{}, + )), + ), + expectPush( + func() eventstore.Command { + success, _ := url.Parse("https://success.url") + failure, _ := url.Parse("https://failure.url") + return idpintent.NewStartedEvent( + context.Background(), + &idpintent.NewAggregate("id", "instance").Aggregate, + success, + failure, + "idp", + ) + }(), + ), + ), + idGenerator: mock.ExpectID(t, "id"), + }, + args{ + ctx: context.Background(), + instanceID: "instance", + idpID: "idp", + successURL: "https://success.url", + failureURL: "https://failure.url", + }, + res{ + intentID: "id", + details: &domain.ObjectDetails{ResourceOwner: "instance"}, + }, + }, + { + "push, instance without org", + fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter( + eventFromEventPusher( + instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + "clientID", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("clientSecret"), + }, + "auth", + "token", + "user", + "idAttribute", + nil, + rep_idp.Options{}, + )), + ), + expectPush( + func() eventstore.Command { + success, _ := url.Parse("https://success.url") + failure, _ := url.Parse("https://failure.url") + return idpintent.NewStartedEvent( + context.Background(), + &idpintent.NewAggregate("id", "instance").Aggregate, + success, + failure, + "idp", + ) + }(), + ), + ), + idGenerator: mock.ExpectID(t, "id"), + }, + args{ + ctx: context.Background(), + instanceID: "instance", + idpID: "idp", + successURL: "https://success.url", + failureURL: "https://failure.url", + }, + res{ + intentID: "id", + details: &domain.ObjectDetails{ResourceOwner: "instance"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } - intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.resourceOwner) + intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID) require.ErrorIs(t, err, tt.res.err) if intentWriteModel != nil { assert.Equal(t, tt.res.intentID, intentWriteModel.AggregateID) @@ -213,7 +339,7 @@ func TestCommands_CreateIntent(t *testing.T) { func TestCommands_AuthFromProvider(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore secretCrypto crypto.EncryptionAlgorithm } type args struct { @@ -238,7 +364,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { "idp not existing", fields{ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -256,7 +382,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { "idp removed", fields{ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithInstanceID( "instance", @@ -300,7 +426,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { "oauth auth redirect", fields{ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithInstanceID( "instance", @@ -360,7 +486,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { "migrated and push", fields{ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithInstanceID( "instance", @@ -450,7 +576,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.secretCrypto, } content, redirect, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL) @@ -463,7 +589,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { func TestCommands_AuthFromProvider_SAML(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore secretCrypto crypto.EncryptionAlgorithm } type args struct { @@ -488,7 +614,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { "saml auth default redirect", fields{ secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithInstanceID( "instance", @@ -534,7 +660,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { failure, _ := url.Parse("https://failure.url") return idpintent.NewStartedEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, success, failure, "idp", @@ -546,7 +672,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { []eventstore.Command{ idpintent.NewSAMLRequestEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, "request", ), }, @@ -572,7 +698,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.secretCrypto, } content, _, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL, tt.args.samlRootURL) @@ -595,7 +721,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { func TestCommands_SucceedIDPIntent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idpConfigEncryption crypto.EncryptionAlgorithm } type args struct { @@ -623,6 +749,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { m.EXPECT().Encrypt(gomock.Any()).Return(nil, zerrors.ThrowInternal(nil, "id", "encryption failed")) return m }(), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -643,6 +770,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { m.EXPECT().Encrypt(gomock.Any()).Return(nil, zerrors.ThrowInternal(nil, "id", "encryption failed")) return m }(), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -663,12 +791,12 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { "push", fields{ idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectPush( func() eventstore.Command { event := idpintent.NewSucceededEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, []byte(`{"sub":"id","preferred_username":"username"}`), "id", "username", @@ -688,7 +816,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), idpSession: &openid.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -712,7 +840,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } got, err := c.SucceedIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.idpSession, tt.args.userID) @@ -724,7 +852,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idpConfigEncryption crypto.EncryptionAlgorithm } type args struct { @@ -752,6 +880,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { m.EXPECT().Encrypt(gomock.Any()).Return(nil, zerrors.ThrowInternal(nil, "id", "encryption failed")) return m }(), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -765,11 +894,11 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { "push", fields{ idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectPush( idpintent.NewSAMLSucceededEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, []byte(`{"sub":"id","preferred_username":"username"}`), "id", "username", @@ -786,7 +915,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), assertion: &saml.Assertion{ID: "id"}, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", @@ -803,11 +932,11 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { "push with userID", fields{ idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectPush( idpintent.NewSAMLSucceededEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, []byte(`{"sub":"id","preferred_username":"username"}`), "id", "username", @@ -824,7 +953,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), assertion: &saml.Assertion{ID: "id"}, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", @@ -842,7 +971,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.assertion) @@ -854,7 +983,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { func TestCommands_RequestSAMLIDPIntent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -873,11 +1002,11 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { { "push", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectPush( idpintent.NewSAMLRequestEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, "request", ), ), @@ -885,7 +1014,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), request: "request", }, res{}, @@ -894,7 +1023,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } err := c.RequestSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.request) require.ErrorIs(t, err, tt.res.err) @@ -905,7 +1034,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idpConfigEncryption crypto.EncryptionAlgorithm } type args struct { @@ -933,10 +1062,11 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { m.EXPECT().Encrypt(gomock.Any()).Return(nil, zerrors.ThrowInternal(nil, "id", "encryption failed")) return m }(), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -946,11 +1076,11 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { "push", fields{ idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectPush( idpintent.NewLDAPSucceededEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, []byte(`{"id":"id","preferredUsername":"username","preferredLanguage":"und"}`), "id", "username", @@ -962,7 +1092,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), attributes: map[string][]string{"id": {"id"}}, idpUser: ldap.NewUser( "id", @@ -988,7 +1118,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.attributes) @@ -1000,7 +1130,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { func TestCommands_FailIDPIntent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1019,11 +1149,11 @@ func TestCommands_FailIDPIntent(t *testing.T) { { "push", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectPush( idpintent.NewFailedEvent( context.Background(), - &idpintent.NewAggregate("id", "ro").Aggregate, + &idpintent.NewAggregate("id", "instance").Aggregate, "reason", ), ), @@ -1031,7 +1161,7 @@ func TestCommands_FailIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "instance"), reason: "reason", }, res{ @@ -1042,7 +1172,7 @@ func TestCommands_FailIDPIntent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } err := c.FailIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.reason) require.ErrorIs(t, err, tt.res.err) diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go index cf5bb7d1e5..a7b2378ec3 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -416,7 +416,7 @@ func prepareAddLoginPolicy(a *org.Aggregate, policy *AddLoginPolicy) preparation return nil, zerrors.ThrowAlreadyExists(nil, "Org-Dgfb2", "Errors.Org.LoginPolicy.AlreadyExists") } for _, idp := range policy.IDPProviders { - exists, err := ExistsIDP(ctx, filter, idp.ConfigID, authz.GetCtxData(ctx).OrgID) + exists, err := ExistsIDPOnOrgOrInstance(ctx, filter, authz.GetInstance(ctx).InstanceID(), a.ResourceOwner, idp.ConfigID) if !exists || err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "Org-FEd32", "Errors.IDPConfig.NotExisting") } diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 16308432b3..58f5f73706 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -683,7 +683,8 @@ func TestCommands_updateSession(t *testing.T) { "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), ), eventFromEventPusher( - idpintent.NewSucceededEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate, + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("id", "instance1").Aggregate, nil, "idpUserID", "idpUserName", @@ -775,7 +776,8 @@ func TestCommands_updateSession(t *testing.T) { "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), ), eventFromEventPusher( - idpintent.NewSucceededEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate, + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("id", "instance1").Aggregate, nil, "idpUserID", "idpUsername", diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 4a7ef97add..41ce13f950 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -6,6 +6,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -294,7 +295,7 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation. } func addLink(ctx context.Context, filter preparation.FilterToQueryReducer, a *user.Aggregate, link *AddLink) (eventstore.Command, error) { - exists, err := ExistsIDP(ctx, filter, link.IDPID, a.ResourceOwner) + exists, err := ExistsIDPOnOrgOrInstance(ctx, filter, authz.GetInstance(ctx).InstanceID(), a.ResourceOwner, link.IDPID) if !exists || err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-39nf2", "Errors.IDPConfig.NotExisting") } diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index fd717c0de4..a24751f570 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -73,6 +73,11 @@ func (c *Commands) createHumanTOTP(ctx context.Context, userID, resourceOwner st logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname") return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-SqyJz", "Errors.User.NotFound") } + if authz.GetCtxData(ctx).UserID != userID { + if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, human.ResourceOwner, userID); err != nil { + return nil, err + } + } org, err := c.getOrg(ctx, human.ResourceOwner) if err != nil { logging.WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname") @@ -124,6 +129,11 @@ func (c *Commands) HumanCheckMFATOTPSetup(ctx context.Context, userID, code, use if err != nil { return nil, err } + if authz.GetCtxData(ctx).UserID != userID { + if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, existingOTP.ResourceOwner, userID); err != nil { + return nil, err + } + } if existingOTP.State == domain.MFAStateUnspecified || existingOTP.State == domain.MFAStateRemoved { return nil, zerrors.ThrowNotFound(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotExisting") } @@ -238,13 +248,15 @@ func (c *Commands) addHumanOTPSMS(ctx context.Context, userID, resourceOwner str if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-QSF2s", "Errors.User.UserIDMissing") } - if err := authz.UserIDInCTX(ctx, userID); err != nil { - return nil, err - } otpWriteModel, err := c.otpSMSWriteModelByID(ctx, userID, resourceOwner) if err != nil { return nil, err } + if authz.GetCtxData(ctx).UserID != userID { + if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, otpWriteModel.ResourceOwner(), userID); err != nil { + return nil, err + } + } if otpWriteModel.otpAdded { return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-Ad3g2", "Errors.User.MFA.OTP.AlreadyReady") } @@ -365,6 +377,11 @@ func (c *Commands) addHumanOTPEmail(ctx context.Context, userID, resourceOwner s if err != nil { return nil, err } + if authz.GetCtxData(ctx).UserID != userID { + if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, otpWriteModel.ResourceOwner(), userID); err != nil { + return nil, err + } + } if otpWriteModel.otpAdded { return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-MKL2s", "Errors.User.MFA.OTP.AlreadyReady") } diff --git a/internal/command/user_human_otp_test.go b/internal/command/user_human_otp_test.go index 838e2357c6..0cfba917c7 100644 --- a/internal/command/user_human_otp_test.go +++ b/internal/command/user_human_otp_test.go @@ -25,7 +25,8 @@ import ( func TestCommandSide_AddHumanTOTP(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + permissionCheck domain.PermissionCheck } type ( args struct { @@ -47,12 +48,10 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), orgID: "org1", userID: "", }, @@ -63,13 +62,12 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), orgID: "org1", userID: "user1", }, @@ -77,11 +75,40 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { err: zerrors.IsPreconditionFailed, }, }, + { + name: "other user, no permission error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + permissionCheck: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + orgID: "org1", + userID: "user2", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, { name: "org not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -100,7 +127,7 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), orgID: "org1", userID: "user1", }, @@ -111,8 +138,7 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { { name: "org iam policy not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -138,7 +164,7 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), orgID: "org1", userID: "user1", }, @@ -149,8 +175,7 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { { name: "otp already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -197,7 +222,7 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), orgID: "org1", userID: "user1", }, @@ -209,7 +234,8 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.permissionCheck, } got, err := r.AddHumanTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -227,7 +253,8 @@ func TestCommandSide_AddHumanTOTP(t *testing.T) { func TestCommands_createHumanTOTP(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -244,23 +271,51 @@ func TestCommands_createHumanTOTP(t *testing.T) { { name: "user not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), resourceOwner: "org1", userID: "user1", }, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SqyJz", "Errors.User.NotFound"), }, + { + name: "other user, no permission error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: authz.NewMockContext("instanceID", "org1", "user1"), + resourceOwner: "org1", + userID: "user2", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, { name: "org not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -281,7 +336,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), resourceOwner: "org1", userID: "user1", }, @@ -290,8 +345,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { { name: "org iam policy not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -321,7 +375,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), resourceOwner: "org1", userID: "user1", }, @@ -330,8 +384,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { { name: "otp already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -385,7 +438,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), resourceOwner: "org1", userID: "user1", }, @@ -394,8 +447,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { { name: "issuer not in context", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -434,7 +486,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContext("instanceID", "org1", "user1"), resourceOwner: "org1", userID: "user1", }, @@ -443,8 +495,7 @@ func TestCommands_createHumanTOTP(t *testing.T) { { name: "success", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -483,17 +534,67 @@ func TestCommands_createHumanTOTP(t *testing.T) { ), }, args: args{ - ctx: authz.WithRequestedDomain(context.Background(), "zitadel.com"), + ctx: authz.WithRequestedDomain(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), resourceOwner: "org1", userID: "user1", }, want: true, }, + { + name: "success, other user", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user2", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &user.NewAggregate("org1", "org1").Aggregate, + "org", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: authz.WithRequestedDomain(authz.NewMockContext("instanceID", "org1", "user1"), "zitadel.com"), + resourceOwner: "org1", + userID: "user2", + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), @@ -522,12 +623,14 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg) require.NoError(t, err) userAgg := &user.NewAggregate("user1", "org1").Aggregate + userAgg2 := &user.NewAggregate("user2", "org1").Aggregate code, err := totp.GenerateCode(key.Secret(), time.Now()) require.NoError(t, err) type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { userID string @@ -542,14 +645,17 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { wantErr error }{ { - name: "missing user id", + name: "missing user id", + fields: fields{ + eventstore: expectEventstore(), + }, args: args{}, wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing"), }, { name: "filter error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilterError(io.ErrClosedPipe), ), }, @@ -559,11 +665,28 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { }, wantErr: io.ErrClosedPipe, }, + { + name: "other user, no permission error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPAddedEvent(ctx, userAgg2, secret), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + resourceOwner: "org1", + userID: "user2", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, { name: "otp not existing error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), @@ -583,8 +706,7 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { { name: "otp already ready error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), @@ -607,8 +729,7 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { { name: "wrong code", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), @@ -626,8 +747,7 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { { name: "push error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), @@ -651,8 +771,7 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { { name: "success", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), @@ -672,11 +791,36 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { userID: "user1", }, }, + { + name: "success, other user", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPAddedEvent(ctx, userAgg2, secret), + ), + ), + expectPush( + user.NewHumanOTPVerifiedEvent(ctx, + userAgg2, + "agent1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + resourceOwner: "org1", + code: code, + userID: "user2", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: cryptoAlg, @@ -695,7 +839,7 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) { func TestCommandSide_RemoveHumanTOTP(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type ( args struct { @@ -717,9 +861,7 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) { { name: "userid missing, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -733,8 +875,7 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) { { name: "otp not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -750,8 +891,7 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) { { name: "otp not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(context.Background(), @@ -782,7 +922,7 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } got, err := r.HumanRemoveTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -801,7 +941,8 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) { func TestCommandSide_AddHumanOTPSMS(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -837,15 +978,24 @@ func TestCommandSide_AddHumanOTPSMS(t *testing.T) { { name: "wrong user, permission denied error", fields: fields{ - eventstore: expectEventstore(), + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPSMSAddedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ ctx: ctx, - userID: "other", + userID: "user2", resourceOwner: "org1", }, res: res{ - err: zerrors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"), + err: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, }, { @@ -954,11 +1104,48 @@ func TestCommandSide_AddHumanOTPSMS(t *testing.T) { }, }, }, + { + name: "successful add, other user", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanPhoneChangedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + "+4179654321", + ), + ), + eventFromEventPusher( + user.NewHumanPhoneVerifiedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + ), + ), + ), + expectPush( + user.NewHumanOTPSMSAddedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: ctx, + userID: "user2", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.AddHumanOTPSMS(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) @@ -1942,7 +2129,8 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) { func TestCommandSide_AddHumanOTPEmail(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") type fields struct { - eventstore func(*testing.T) *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -1975,6 +2163,29 @@ func TestCommandSide_AddHumanOTPEmail(t *testing.T) { err: zerrors.ThrowInvalidArgument(nil, "COMMAND-Sg1hz", "Errors.User.UserIDMissing"), }, }, + { + name: "wrong user, permission denied error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPEmailAddedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: ctx, + userID: "user2", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + }, { name: "otp email already exists, already exists error", fields: fields{ @@ -2048,11 +2259,48 @@ func TestCommandSide_AddHumanOTPEmail(t *testing.T) { }, }, }, + { + name: "successful add, other user", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanEmailChangedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + "email@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + ), + ), + ), + expectPush( + user.NewHumanOTPEmailAddedEvent(ctx, + &user.NewAggregate("user2", "org1").Aggregate, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: ctx, + userID: "user2", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore(t), + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.AddHumanOTPEmail(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) assert.ErrorIs(t, err, tt.res.err) diff --git a/internal/command/user_human_webauthn.go b/internal/command/user_human_webauthn.go index 5588ed7286..6988b0052a 100644 --- a/internal/command/user_human_webauthn.go +++ b/internal/command/user_human_webauthn.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -77,7 +78,7 @@ func (c *Commands) getHumanPasswordlessLogin(ctx context.Context, userID, authRe }, nil } -func (c *Commands) HumanAddU2FSetup(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) { +func (c *Commands) HumanAddU2FSetup(ctx context.Context, userID, resourceowner string) (*domain.WebAuthNToken, error) { u2fTokens, err := c.getHumanU2FTokens(ctx, userID, resourceowner) if err != nil { return nil, err @@ -103,7 +104,7 @@ func (c *Commands) HumanAddU2FSetup(ctx context.Context, userID, resourceowner s return createdWebAuthN, nil } -func (c *Commands) HumanAddPasswordlessSetup(ctx context.Context, userID, resourceowner string, isLoginUI bool, authenticatorPlatform domain.AuthenticatorAttachment) (*domain.WebAuthNToken, error) { +func (c *Commands) HumanAddPasswordlessSetup(ctx context.Context, userID, resourceowner string, authenticatorPlatform domain.AuthenticatorAttachment) (*domain.WebAuthNToken, error) { passwordlessTokens, err := c.getHumanPasswordlessTokens(ctx, userID, resourceowner) if err != nil { return nil, err @@ -134,7 +135,7 @@ func (c *Commands) HumanAddPasswordlessSetupInitCode(ctx context.Context, userID if err != nil { return nil, err } - return c.HumanAddPasswordlessSetup(ctx, userID, resourceowner, true, preferredPlatformType) + return c.HumanAddPasswordlessSetup(ctx, userID, resourceowner, preferredPlatformType) } func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner, rpID string, tokens []*domain.WebAuthNToken, authenticatorPlatform domain.AuthenticatorAttachment, userVerification domain.UserVerificationRequirement) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) { @@ -145,6 +146,11 @@ func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner, if err != nil { return nil, nil, nil, err } + if authz.GetCtxData(ctx).UserID != userID { + if err = c.checkPermission(ctx, domain.PermissionUserCredentialWrite, user.ResourceOwner, userID); err != nil { + return nil, nil, nil, err + } + } org, err := c.getOrg(ctx, user.ResourceOwner) if err != nil { return nil, nil, nil, err @@ -179,7 +185,7 @@ func (c *Commands) HumanVerifyU2FSetup(ctx context.Context, userID, resourceowne if err != nil { return nil, err } - userAgg, webAuthN, verifyWebAuthN, err := c.verifyHumanWebAuthN(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, u2fTokens) + userAgg, webAuthN, verifyWebAuthN, err := c.verifyHumanWebAuthN(ctx, userID, resourceowner, tokenName, credentialData, u2fTokens) if err != nil { return nil, err } @@ -230,7 +236,7 @@ func (c *Commands) humanHumanPasswordlessSetup(ctx context.Context, userID, reso if err != nil { return nil, err } - userAgg, webAuthN, verifyWebAuthN, err := c.verifyHumanWebAuthN(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, u2fTokens) + userAgg, webAuthN, verifyWebAuthN, err := c.verifyHumanWebAuthN(ctx, userID, resourceowner, tokenName, credentialData, u2fTokens) if err != nil { return nil, err } @@ -263,7 +269,7 @@ func (c *Commands) humanHumanPasswordlessSetup(ctx context.Context, userID, reso return writeModelToObjectDetails(&verifyWebAuthN.WriteModel), nil } -func (c *Commands) verifyHumanWebAuthN(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte, tokens []*domain.WebAuthNToken) (*eventstore.Aggregate, *domain.WebAuthNToken, *HumanWebAuthNWriteModel, error) { +func (c *Commands) verifyHumanWebAuthN(ctx context.Context, userID, resourceowner, tokenName string, credentialData []byte, tokens []*domain.WebAuthNToken) (*eventstore.Aggregate, *domain.WebAuthNToken, *HumanWebAuthNWriteModel, error) { if userID == "" { return nil, nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing") } @@ -272,7 +278,7 @@ func (c *Commands) verifyHumanWebAuthN(ctx context.Context, userID, resourceowne return nil, nil, nil, err } _, token := domain.GetTokenToVerify(tokens) - webAuthN, err := c.webauthnConfig.FinishRegistration(ctx, user, token, tokenName, credentialData, userAgentID != "") + webAuthN, err := c.webauthnConfig.FinishRegistration(ctx, user, token, tokenName, credentialData) if err != nil { return nil, nil, nil, err } diff --git a/internal/command/user_v2_passkey_test.go b/internal/command/user_v2_passkey_test.go index 15286bea11..7b1343b862 100644 --- a/internal/command/user_v2_passkey_test.go +++ b/internal/command/user_v2_passkey_test.go @@ -339,7 +339,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) { } func TestCommands_pushUserPasskey(t *testing.T) { - ctx := authz.WithRequestedDomain(context.Background(), "example.com") + ctx := authz.WithRequestedDomain(authz.NewMockContext("instance1", "org1", "user1"), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, diff --git a/internal/command/user_v2_totp.go b/internal/command/user_v2_totp.go index 61514b0e7e..0ceca9b073 100644 --- a/internal/command/user_v2_totp.go +++ b/internal/command/user_v2_totp.go @@ -3,15 +3,11 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" ) -func (c *Commands) AddUserTOTP(ctx context.Context, userID, resourceowner string) (*domain.TOTP, error) { - if err := authz.UserIDInCTX(ctx, userID); err != nil { - return nil, err - } - prep, err := c.createHumanTOTP(ctx, userID, resourceowner) +func (c *Commands) AddUserTOTP(ctx context.Context, userID, resourceOwner string) (*domain.TOTP, error) { + prep, err := c.createHumanTOTP(ctx, userID, resourceOwner) if err != nil { return nil, err } @@ -26,8 +22,5 @@ func (c *Commands) AddUserTOTP(ctx context.Context, userID, resourceowner string } func (c *Commands) CheckUserTOTP(ctx context.Context, userID, code, resourceOwner string) (*domain.ObjectDetails, error) { - if err := authz.UserIDInCTX(ctx, userID); err != nil { - return nil, err - } return c.HumanCheckMFATOTPSetup(ctx, userID, code, "", resourceOwner) } diff --git a/internal/command/user_v2_totp_test.go b/internal/command/user_v2_totp_test.go index 4e1df191d5..d05bdc9647 100644 --- a/internal/command/user_v2_totp_test.go +++ b/internal/command/user_v2_totp_test.go @@ -23,9 +23,11 @@ import ( func TestCommands_AddUserTOTP(t *testing.T) { ctx := authz.NewMockContext("inst1", "org1", "user1") userAgg := &user.NewAggregate("user1", "org1").Aggregate + userAgg2 := &user.NewAggregate("user2", "org1").Aggregate type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { userID string @@ -39,12 +41,33 @@ func TestCommands_AddUserTOTP(t *testing.T) { wantErr error }{ { - name: "wrong user", + name: "other user, permission error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, args: args{ userID: "foo", resourceowner: "org1", }, - wantErr: zerrors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"), + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), }, { name: "create otp error", @@ -53,8 +76,7 @@ func TestCommands_AddUserTOTP(t *testing.T) { resourceowner: "org1", }, fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -67,8 +89,7 @@ func TestCommands_AddUserTOTP(t *testing.T) { resourceowner: "org1", }, fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(ctx, @@ -118,8 +139,7 @@ func TestCommands_AddUserTOTP(t *testing.T) { resourceowner: "org1", }, fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(ctx, @@ -162,11 +182,63 @@ func TestCommands_AddUserTOTP(t *testing.T) { }, want: true, }, + { + name: "success, other user", + args: args{ + userID: "user2", + resourceowner: "org1", + }, + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + userAgg2, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(ctx, + userAgg2, + "org", + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(ctx, + userAgg2, + true, + true, + true, + ), + ), + ), + expectFilter(), + expectRandomPush([]eventstore.Command{ + user.NewHumanOTPAddedEvent(ctx, userAgg2, nil), + }), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ Issuer: "zitadel.com", @@ -198,7 +270,8 @@ func TestCommands_CheckUserTOTP(t *testing.T) { require.NoError(t, err) type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { userID string @@ -213,17 +286,46 @@ func TestCommands_CheckUserTOTP(t *testing.T) { wantErr error }{ { - name: "wrong user id", + name: "other user, no permission, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPAddedEvent(ctx, userAgg, secret), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, args: args{ userID: "foo", }, - wantErr: zerrors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"), + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "user id, with permission, success", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanOTPAddedEvent(ctx, &user.NewAggregate("foo", "org1").Aggregate, secret), + ), + ), + expectPush( + user.NewHumanOTPVerifiedEvent(ctx, &user.NewAggregate("foo", "org1").Aggregate, ""), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "foo", + code: code, + }, }, { name: "success", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanOTPAddedEvent(ctx, userAgg, secret), @@ -244,7 +346,8 @@ func TestCommands_CheckUserTOTP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, multifactors: domain.MultifactorConfigs{ OTP: domain.OTPConfig{ CryptoMFA: cryptoAlg, diff --git a/internal/command/user_v2_u2f.go b/internal/command/user_v2_u2f.go index 50c16aa30f..41ab47373d 100644 --- a/internal/command/user_v2_u2f.go +++ b/internal/command/user_v2_u2f.go @@ -3,16 +3,12 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/user" ) func (c *Commands) RegisterUserU2F(ctx context.Context, userID, resourceOwner, rpID string) (*domain.WebAuthNRegistrationDetails, error) { - if err := authz.UserIDInCTX(ctx, userID); err != nil { - return nil, err - } return c.registerUserU2F(ctx, userID, resourceOwner, rpID) } diff --git a/internal/command/user_v2_u2f_test.go b/internal/command/user_v2_u2f_test.go index a69dac062e..b2a5788d00 100644 --- a/internal/command/user_v2_u2f_test.go +++ b/internal/command/user_v2_u2f_test.go @@ -1,7 +1,6 @@ package command import ( - "context" "io" "testing" @@ -30,8 +29,9 @@ func TestCommands_RegisterUserU2F(t *testing.T) { } userAgg := &user.NewAggregate("user1", "org1").Aggregate type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + permissionCheck domain.PermissionCheck } type args struct { userID string @@ -45,18 +45,10 @@ func TestCommands_RegisterUserU2F(t *testing.T) { want *domain.WebAuthNRegistrationDetails wantErr error }{ - { - name: "wrong user", - args: args{ - userID: "foo", - resourceOwner: "org1", - }, - wantErr: zerrors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"), - }, { name: "get human passwordless error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilterError(io.ErrClosedPipe), ), }, @@ -66,10 +58,38 @@ func TestCommands_RegisterUserU2F(t *testing.T) { }, wantErr: io.ErrClosedPipe, }, + { + name: "other user, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectFilter(eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + userAgg, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + )), + ), + permissionCheck: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "foo", + resourceOwner: "org1", + }, + wantErr: zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, { name: "id generator error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), // getHumanPasswordlessTokens expectFilter(eventFromEventPusher( user.NewHumanAddedEvent(ctx, @@ -110,9 +130,10 @@ func TestCommands_RegisterUserU2F(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - webauthnConfig: webauthnConfig, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.permissionCheck, + webauthnConfig: webauthnConfig, } _, err := c.RegisterUserU2F(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.rpID) require.ErrorIs(t, err, tt.wantErr) @@ -122,7 +143,7 @@ func TestCommands_RegisterUserU2F(t *testing.T) { } func TestCommands_pushUserU2F(t *testing.T) { - ctx := authz.WithRequestedDomain(context.Background(), "example.com") + ctx := authz.WithRequestedDomain(authz.NewMockContext("instance1", "org1", "user1"), "example.com") webauthnConfig := &webauthn_helper.Config{ DisplayName: "test", ExternalSecure: true, diff --git a/internal/domain/execution.go b/internal/domain/execution.go index 08f215bf08..eb83a0578c 100644 --- a/internal/domain/execution.go +++ b/internal/domain/execution.go @@ -31,3 +31,17 @@ func (e ExecutionType) String() string { } return "" } + +type ExecutionTargetType uint + +func (s ExecutionTargetType) Valid() bool { + return s < executionTargetTypeStateCount +} + +const ( + ExecutionTargetTypeUnspecified ExecutionTargetType = iota + ExecutionTargetTypeInclude + ExecutionTargetTypeTarget + + executionTargetTypeStateCount +) diff --git a/internal/domain/permission.go b/internal/domain/permission.go index fd20f77cf0..cf2d02d426 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -27,9 +27,10 @@ func (p *Permissions) appendPermission(ctxID, permission string) { type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error) const ( - PermissionUserWrite = "user.write" - PermissionUserRead = "user.read" - PermissionUserDelete = "user.delete" - PermissionSessionWrite = "session.write" - PermissionSessionDelete = "session.delete" + PermissionUserWrite = "user.write" + PermissionUserRead = "user.read" + PermissionUserDelete = "user.delete" + PermissionUserCredentialWrite = "user.credential.write" + PermissionSessionWrite = "session.write" + PermissionSessionDelete = "session.delete" ) diff --git a/internal/domain/target.go b/internal/domain/target.go index 83ab85478e..1e08f91cf0 100644 --- a/internal/domain/target.go +++ b/internal/domain/target.go @@ -4,7 +4,8 @@ type TargetType uint const ( TargetTypeWebhook TargetType = iota - TargetTypeRequestResponse + TargetTypeCall + TargetTypeAsync ) type TargetState int32 diff --git a/internal/execution/execution.go b/internal/execution/execution.go new file mode 100644 index 0000000000..abb2153fc2 --- /dev/null +++ b/internal/execution/execution.go @@ -0,0 +1,122 @@ +package execution + +import ( + "bytes" + "context" + "io" + "net/http" + "time" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type ContextInfo interface { + GetHTTPRequestBody() []byte + GetContent() interface{} + SetHTTPResponseBody([]byte) error +} + +type Target interface { + GetTargetID() string + IsInterruptOnError() bool + GetEndpoint() string + GetTargetType() domain.TargetType + GetTimeout() time.Duration +} + +// CallTargets call a list of targets in order with handling of error and responses +func CallTargets( + ctx context.Context, + targets []Target, + info ContextInfo, +) (_ interface{}, err error) { + ctx, span := tracing.NewSpan(ctx) + defer span.EndWithError(err) + + for _, target := range targets { + // call the type of target + resp, err := CallTarget(ctx, target, info) + // handle error if interrupt is set + if err != nil && target.IsInterruptOnError() { + return nil, err + } + if len(resp) > 0 { + // error in unmarshalling + if err := info.SetHTTPResponseBody(resp); err != nil { + return nil, err + } + } + } + return info.GetContent(), nil +} + +type ContextInfoRequest interface { + GetHTTPRequestBody() []byte +} + +// CallTarget call the desired type of target with handling of responses +func CallTarget( + ctx context.Context, + target Target, + info ContextInfoRequest, +) (res []byte, err error) { + ctx, span := tracing.NewSpan(ctx) + defer span.EndWithError(err) + + switch target.GetTargetType() { + // get request, ignore response and return request and error for handling in list of targets + case domain.TargetTypeWebhook: + return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()) + // get request, return response and error + case domain.TargetTypeCall: + return call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()) + case domain.TargetTypeAsync: + go func(target Target, info ContextInfoRequest) { + if _, err := call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil { + logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err) + } + }(target, info) + return nil, nil + default: + return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown") + } +} + +// webhook call a webhook, ignore the response but return the errror +func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error { + _, err := call(ctx, url, timeout, body) + return err +} + +// call function to do a post HTTP request to a desired url with timeout +func call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, span := tracing.NewSpan(ctx) + defer func() { + cancel() + span.EndWithError(err) + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + client := http.DefaultClient + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Check for success between 200 and 299, redirect 300 to 399 is handled by the client, return error with statusCode >= 400 + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + return io.ReadAll(resp.Body) + } + return nil, zerrors.ThrowUnknown(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed") +} diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go new file mode 100644 index 0000000000..2d891148df --- /dev/null +++ b/internal/execution/execution_test.go @@ -0,0 +1,347 @@ +package execution + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" +) + +var _ Target = &mockTarget{} + +type mockTarget struct { + InstanceID string + ExecutionID string + TargetID string + TargetType domain.TargetType + Endpoint string + Timeout time.Duration + InterruptOnError bool +} + +func (e *mockTarget) GetTargetID() string { + return e.TargetID +} +func (e *mockTarget) IsInterruptOnError() bool { + return e.InterruptOnError +} +func (e *mockTarget) GetEndpoint() string { + return e.Endpoint +} +func (e *mockTarget) GetTargetType() domain.TargetType { + return e.TargetType +} +func (e *mockTarget) GetTimeout() time.Duration { + return e.Timeout +} + +func Test_Call(t *testing.T) { + type args struct { + ctx context.Context + timeout time.Duration + sleep time.Duration + method string + body []byte + respBody []byte + statusCode int + } + type res struct { + body []byte + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "not ok status", + args{ + ctx: context.Background(), + timeout: time.Minute, + sleep: time.Second, + method: http.MethodPost, + body: []byte("{\"request\": \"values\"}"), + respBody: []byte("{\"response\": \"values\"}"), + statusCode: http.StatusBadRequest, + }, + res{ + wantErr: true, + }, + }, + { + "timeout", + args{ + ctx: context.Background(), + timeout: time.Second, + sleep: time.Second, + method: http.MethodPost, + body: []byte("{\"request\": \"values\"}"), + respBody: []byte("{\"response\": \"values\"}"), + statusCode: http.StatusOK, + }, + res{ + wantErr: true, + }, + }, + { + "ok", + args{ + ctx: context.Background(), + timeout: time.Minute, + sleep: time.Second, + method: http.MethodPost, + body: []byte("{\"request\": \"values\"}"), + respBody: []byte("{\"response\": \"values\"}"), + statusCode: http.StatusOK, + }, + res{ + body: []byte("{\"response\": \"values\"}"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + respBody, err := testServerCall(t, + tt.args.method, + tt.args.body, + tt.args.sleep, + tt.args.statusCode, + tt.args.respBody, + testCall(tt.args.ctx, tt.args.timeout, tt.args.body), + ) + if tt.res.wantErr { + assert.Error(t, err) + assert.Nil(t, respBody) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.res.body, respBody) + } + }) + } +} + +func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) { + return func(url string) ([]byte, error) { + return call(ctx, url, timeout, body) + } +} + +func testCallTarget(ctx context.Context, + target *mockTarget, + info ContextInfoRequest, +) func(string) ([]byte, error) { + return func(url string) (r []byte, err error) { + target.Endpoint = url + return CallTarget(ctx, target, info) + } +} + +func testServerCall( + t *testing.T, + method string, + body []byte, + timeout time.Duration, + statusCode int, + respBody []byte, + call func(string) ([]byte, error), +) ([]byte, error) { + handler := func(w http.ResponseWriter, r *http.Request) { + checkRequest(t, r, method, body) + + if statusCode != http.StatusOK { + http.Error(w, "error", statusCode) + return + } + + time.Sleep(timeout) + + w.Header().Set("Content-Type", "application/json") + if _, err := io.WriteString(w, string(respBody)); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + return call(server.URL) +} + +func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) { + sentBody, err := io.ReadAll(sent.Body) + require.NoError(t, err) + require.Equal(t, expectedBody, sentBody) + require.Equal(t, method, sent.Method) +} + +var _ ContextInfoRequest = &mockContextInfoRequest{} + +type request struct { + Request string `json:"request"` +} + +type mockContextInfoRequest struct { + Request *request `json:"request"` +} + +func newMockContextInfoRequest(s string) *mockContextInfoRequest { + return &mockContextInfoRequest{&request{s}} +} + +func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte { + data, _ := json.Marshal(c) + return data +} + +func (c *mockContextInfoRequest) GetContent() []byte { + data, _ := json.Marshal(c.Request) + return data +} + +func Test_CallTarget(t *testing.T) { + type args struct { + ctx context.Context + target *mockTarget + sleep time.Duration + + info ContextInfoRequest + + method string + body []byte + + respBody []byte + statusCode int + } + type res struct { + body []byte + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "unknown targettype, error", + args{ + ctx: context.Background(), + sleep: time.Second, + method: http.MethodPost, + info: newMockContextInfoRequest("content1"), + target: &mockTarget{ + TargetType: 4, + }, + body: []byte("{\"request\":{\"request\":\"content1\"}}"), + respBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusInternalServerError, + }, + res{ + wantErr: true, + }, + }, + { + "webhook, error", + args{ + ctx: context.Background(), + sleep: time.Second, + method: http.MethodPost, + info: newMockContextInfoRequest("content1"), + target: &mockTarget{ + TargetType: domain.TargetTypeWebhook, + Timeout: time.Minute, + }, + body: []byte("{\"request\":{\"request\":\"content1\"}}"), + respBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusInternalServerError, + }, + res{ + wantErr: true, + }, + }, + { + "webhook, ok", + args{ + ctx: context.Background(), + sleep: time.Second, + method: http.MethodPost, + info: newMockContextInfoRequest("content1"), + target: &mockTarget{ + TargetType: domain.TargetTypeWebhook, + Timeout: time.Minute, + }, + body: []byte("{\"request\":{\"request\":\"content1\"}}"), + respBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusOK, + }, + res{ + body: nil, + }, + }, + { + "request response, error", + args{ + ctx: context.Background(), + sleep: time.Second, + method: http.MethodPost, + info: newMockContextInfoRequest("content1"), + target: &mockTarget{ + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + }, + body: []byte("{\"request\":{\"request\":\"content1\"}}"), + respBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusInternalServerError, + }, + res{ + wantErr: true, + }, + }, + { + "request response, ok", + args{ + ctx: context.Background(), + sleep: time.Second, + method: http.MethodPost, + info: newMockContextInfoRequest("content1"), + target: &mockTarget{ + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + }, + body: []byte("{\"request\":{\"request\":\"content1\"}}"), + respBody: []byte("{\"request\":\"content2\"}"), + statusCode: http.StatusOK, + }, + res{ + body: []byte("{\"request\":\"content2\"}"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + respBody, err := testServerCall(t, + tt.args.method, + tt.args.body, + tt.args.sleep, + tt.args.statusCode, + tt.args.respBody, + testCallTarget(tt.args.ctx, tt.args.target, tt.args.info), + ) + if tt.res.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.res.body, respBody) + }) + } +} diff --git a/internal/integration/assert.go b/internal/integration/assert.go index d5b7353aa2..225b3399b4 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -65,7 +65,9 @@ func AssertListDetails[D ListDetailsMsg](t testing.TB, expected, actual D) { assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult()) - gotCD := gotDetails.GetTimestamp().AsTime() - wantCD := time.Now() - assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute)) + if wantDetails.GetTimestamp() != nil { + gotCD := gotDetails.GetTimestamp().AsTime() + wantCD := time.Now() + assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute)) + } } diff --git a/internal/integration/client.go b/internal/integration/client.go index eb95134bc3..3ebd4fea51 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -288,8 +288,8 @@ func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, c logging.OnError(err).Fatal("set user password") } -func (s *Tester) AddGenericOAuthProvider(t *testing.T) string { - ctx := authz.WithInstance(context.Background(), s.Instance) +func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) string { + ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{ Name: "idp", ClientID: "clientID", @@ -310,8 +310,31 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T) string { return id } -func (s *Tester) AddSAMLProvider(t *testing.T) string { - ctx := authz.WithInstance(context.Background(), s.Instance) +func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, orgID string) string { + ctx = authz.WithInstance(ctx, s.Instance) + id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, orgID, + command.GenericOAuthProvider{ + Name: "idp", + ClientID: "clientID", + ClientSecret: "clientSecret", + AuthorizationEndpoint: "https://example.com/oauth/v2/authorize", + TokenEndpoint: "https://example.com/oauth/v2/token", + UserEndpoint: "https://api.example.com/user", + Scopes: []string{"openid", "profile", "email"}, + IDAttribute: "id", + IDPOptions: idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + require.NoError(t, err) + return id +} + +func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string { + ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ Name: "saml-idp", Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), @@ -326,8 +349,8 @@ func (s *Tester) AddSAMLProvider(t *testing.T) string { return id } -func (s *Tester) AddSAMLRedirectProvider(t *testing.T) string { - ctx := authz.WithInstance(context.Background(), s.Instance) +func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context) string { + ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ Name: "saml-idp-redirect", Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", @@ -343,8 +366,8 @@ func (s *Tester) AddSAMLRedirectProvider(t *testing.T) string { return id } -func (s *Tester) AddSAMLPostProvider(t *testing.T) string { - ctx := authz.WithInstance(context.Background(), s.Instance) +func (s *Tester) AddSAMLPostProvider(t *testing.T, ctx context.Context) string { + ctx = authz.WithInstance(ctx, s.Instance) id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{ Name: "saml-idp-post", Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", @@ -360,17 +383,17 @@ func (s *Tester) AddSAMLPostProvider(t *testing.T) string { return id } -func (s *Tester) CreateIntent(t *testing.T, idpID string) string { - ctx := authz.WithInstance(context.Background(), s.Instance) - writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Organisation.ID) +func (s *Tester) CreateIntent(t *testing.T, ctx context.Context, idpID string) string { + ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) + writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Instance.InstanceID()) require.NoError(t, err) return writeModel.AggregateID } -func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { - ctx := authz.WithInstance(context.Background(), s.Instance) - intentID := s.CreateIntent(t, idpID) - writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID) +func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { + ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) + intentID := s.CreateIntent(t, ctx, idpID) + writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) require.NoError(t, err) idpUser := openid.NewUser( &oidc.UserInfo{ @@ -393,10 +416,10 @@ func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, idpID, userID, idpUse return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } -func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { - ctx := authz.WithInstance(context.Background(), s.Instance) - intentID := s.CreateIntent(t, idpID) - writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID) +func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { + ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) + intentID := s.CreateIntent(t, ctx, idpID) + writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) require.NoError(t, err) username := "username" lang := language.Make("en") @@ -421,10 +444,10 @@ func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, idpID, userID, idpUser return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } -func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { - ctx := authz.WithInstance(context.Background(), s.Instance) - intentID := s.CreateIntent(t, idpID) - writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID) +func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, ctx context.Context, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { + ctx = authz.WithInstance(context.WithoutCancel(ctx), s.Instance) + intentID := s.CreateIntent(t, ctx, idpID) + writeModel, err := s.Server.Commands.GetIntentWriteModel(ctx, intentID, s.Instance.InstanceID()) require.NoError(t, err) idpUser := &saml.UserMapper{ @@ -522,39 +545,32 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj require.NoError(t, err) } -func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *action.CreateTargetResponse { - req := &action.CreateTargetRequest{ - Name: fmt.Sprint(time.Now().UnixNano() + 1), - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, - }, - Timeout: durationpb.New(10 * time.Second), +func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { + nameSet := fmt.Sprint(time.Now().UnixNano() + 1) + if name != "" { + nameSet = name } - target, err := s.Client.ActionV3.CreateTarget(ctx, req) - require.NoError(t, err) - return target -} - -func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T, name string, async bool, interrupt bool) *action.CreateTargetResponse { req := &action.CreateTargetRequest{ - Name: name, - TargetType: &action.CreateTargetRequest_RestWebhook{ - RestWebhook: &action.SetRESTWebhook{ - Url: "https://example.com", - }, - }, - Timeout: durationpb.New(10 * time.Second), + Name: nameSet, + Endpoint: endpoint, + Timeout: durationpb.New(10 * time.Second), } - if async { - req.ExecutionType = &action.CreateTargetRequest_IsAsync{ - IsAsync: true, + switch ty { + case domain.TargetTypeWebhook: + req.TargetType = &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.SetRESTWebhook{ + InterruptOnError: interrupt, + }, } - } - if interrupt { - req.ExecutionType = &action.CreateTargetRequest_InterruptOnError{ - InterruptOnError: true, + case domain.TargetTypeCall: + req.TargetType = &action.CreateTargetRequest_RestCall{ + RestCall: &action.SetRESTCall{ + InterruptOnError: interrupt, + }, + } + case domain.TargetTypeAsync: + req.TargetType = &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.SetRESTAsync{}, } } target, err := s.Client.ActionV3.CreateTarget(ctx, req) @@ -562,16 +578,22 @@ func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T, return target } -func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string, includes []string) *action.SetExecutionResponse { +func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse { target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, Targets: targets, - Includes: includes, }) require.NoError(t, err) return target } +func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { + _, err := s.Client.ActionV3.DeleteExecution(ctx, &action.DeleteExecutionRequest{ + Condition: cond, + }) + require.NoError(t, err) +} + func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse { return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1)) } diff --git a/internal/query/execution.go b/internal/query/execution.go index 5a8a7e0f79..ff501f8201 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -3,7 +3,10 @@ package query import ( "context" "database/sql" + _ "embed" + "encoding/json" "errors" + "time" sq "github.com/Masterminds/squirrel" @@ -11,6 +14,8 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query/projection" + exec "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -23,18 +28,10 @@ var ( name: projection.ExecutionIDCol, table: executionTable, } - ExecutionColumnCreationDate = Column{ - name: projection.ExecutionCreationDateCol, - table: executionTable, - } ExecutionColumnChangeDate = Column{ name: projection.ExecutionChangeDateCol, table: executionTable, } - ExecutionColumnResourceOwner = Column{ - name: projection.ExecutionResourceOwnerCol, - table: executionTable, - } ExecutionColumnInstanceID = Column{ name: projection.ExecutionInstanceIDCol, table: executionTable, @@ -43,14 +40,33 @@ var ( name: projection.ExecutionSequenceCol, table: executionTable, } - ExecutionColumnTargets = Column{ - name: projection.ExecutionTargetsCol, - table: executionTable, + + executionTargetsTable = table{ + name: projection.ExecutionTable + "_" + projection.ExecutionTargetSuffix, + instanceIDCol: projection.ExecutionTargetInstanceIDCol, } - ExecutionColumnIncludes = Column{ - name: projection.ExecutionIncludesCol, - table: executionTable, + executionTargetsTableAlias = executionTargetsTable.setAlias("execution_targets") + ExecutionTargetsColumnInstanceID = Column{ + name: projection.ExecutionTargetInstanceIDCol, + table: executionTargetsTableAlias, } + ExecutionTargetsColumnExecutionID = Column{ + name: projection.ExecutionTargetExecutionIDCol, + table: executionTargetsTableAlias, + } + executionTargetsListCol = Column{ + name: "targets", + table: executionTargetsTableAlias, + } +) + +var ( + //go:embed execution_targets.sql + executionTargetsQuery string + //go:embed targets_by_execution_id.sql + TargetsByExecutionIDQuery string + //go:embed targets_by_execution_ids.sql + TargetsByExecutionIDsQuery string ) type Executions struct { @@ -66,8 +82,7 @@ type Execution struct { ID string domain.ObjectDetails - Targets database.TextArray[string] - Includes database.TextArray[string] + Targets []*exec.Target } type ExecutionSearchQueries struct { @@ -108,84 +123,301 @@ func NewExecutionTypeSearchQuery(t domain.ExecutionType) (SearchQuery, error) { return NewTextQuery(ExecutionColumnID, t.String(), TextStartsWith) } -func NewExecutionTargetSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(ExecutionColumnTargets, value, TextListContains) +func NewTargetSearchQuery(target string) (SearchQuery, error) { + data, err := targetItemJSONB(domain.ExecutionTargetTypeTarget, target) + if err != nil { + return nil, err + } + return NewListContains(executionTargetsListCol, data) } -func NewExecutionIncludeSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(ExecutionColumnIncludes, value, TextListContains) +func NewIncludeSearchQuery(include string) (SearchQuery, error) { + data, err := targetItemJSONB(domain.ExecutionTargetTypeInclude, include) + if err != nil { + return nil, err + } + return NewListContains(executionTargetsListCol, data) } -func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { - return sq.Select( - ExecutionColumnID.identifier(), - ExecutionColumnChangeDate.identifier(), - ExecutionColumnResourceOwner.identifier(), - ExecutionColumnSequence.identifier(), - ExecutionColumnTargets.identifier(), - ExecutionColumnIncludes.identifier(), - countColumn.identifier(), - ).From(executionTable.identifier()). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*Executions, error) { - executions := make([]*Execution, 0) - var count uint64 - for rows.Next() { - execution := new(Execution) - err := rows.Scan( - &execution.ID, - &execution.EventDate, - &execution.ResourceOwner, - &execution.Sequence, - &execution.Targets, - &execution.Includes, - &count, - ) - if err != nil { - return nil, err - } - executions = append(executions, execution) - } +// marshall executionTargets into the same JSONB structure as in the SQL queries +func targetItemJSONB(t domain.ExecutionTargetType, targetItem string) ([]byte, error) { + var target *executionTarget + switch t { + case domain.ExecutionTargetTypeTarget: + target = &executionTarget{Target: targetItem} + case domain.ExecutionTargetTypeInclude: + target = &executionTarget{Include: targetItem} + case domain.ExecutionTargetTypeUnspecified: + return nil, nil + default: + return nil, nil + } + return json.Marshal([]*executionTarget{target}) +} - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-72xfx5jlj7", "Errors.Query.CloseRows") - } +// TargetsByExecutionID query list of targets for best match of a list of IDs, for example: +// [ "request/zitadel.action.v3alpha.ActionService/GetTargetByID", +// "request/zitadel.action.v3alpha.ActionService", +// "request" ] +func (q *Queries) TargetsByExecutionID(ctx context.Context, ids []string) (execution []*ExecutionTarget, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.End() }() - return &Executions{ - Executions: executions, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil - } + instanceID := authz.GetInstance(ctx).InstanceID() + if instanceID == "" { + return nil, nil + } + + err = q.client.QueryContext(ctx, + func(rows *sql.Rows) error { + execution, err = scanExecutionTargets(rows) + return err + }, + TargetsByExecutionIDQuery, + instanceID, + database.TextArray[string](ids), + ) + return execution, err +} + +// TargetsByExecutionIDs query list of targets for best matches of 2 separate lists of IDs, combined for performance, for example: +// [ "request/zitadel.action.v3alpha.ActionService/GetTargetByID", +// "request/zitadel.action.v3alpha.ActionService", +// "request" ] +// and +// [ "response/zitadel.action.v3alpha.ActionService/GetTargetByID", +// "response/zitadel.action.v3alpha.ActionService", +// "response" ] +func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*ExecutionTarget, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.End() }() + + instanceID := authz.GetInstance(ctx).InstanceID() + if instanceID == "" { + return nil, nil + } + + err = q.client.QueryContext(ctx, + func(rows *sql.Rows) error { + execution, err = scanExecutionTargets(rows) + return err + }, + TargetsByExecutionIDsQuery, + instanceID, + database.TextArray[string](ids1), + database.TextArray[string](ids2), + ) + return execution, err } func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { return sq.Select( + ExecutionColumnInstanceID.identifier(), ExecutionColumnID.identifier(), ExecutionColumnChangeDate.identifier(), - ExecutionColumnResourceOwner.identifier(), ExecutionColumnSequence.identifier(), - ExecutionColumnTargets.identifier(), - ExecutionColumnIncludes.identifier(), + executionTargetsListCol.identifier(), ).From(executionTable.identifier()). + Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " + + ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " + + ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(), + ). PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*Execution, error) { - execution := new(Execution) - err := row.Scan( - &execution.ID, - &execution.EventDate, - &execution.ResourceOwner, - &execution.Sequence, - &execution.Targets, - &execution.Includes, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound") - } - return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal") - } - return execution, nil - } + scanExecution +} + +func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { + return sq.Select( + ExecutionColumnInstanceID.identifier(), + ExecutionColumnID.identifier(), + ExecutionColumnChangeDate.identifier(), + ExecutionColumnSequence.identifier(), + executionTargetsListCol.identifier(), + countColumn.identifier(), + ).From(executionTable.identifier()). + Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " + + ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " + + ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(), + ). + PlaceholderFormat(sq.Dollar), + scanExecutions +} + +type executionTarget struct { + Position int `json:"position,omitempty"` + Include string `json:"include,omitempty"` + Target string `json:"target,omitempty"` +} + +func scanExecution(row *sql.Row) (*Execution, error) { + execution := new(Execution) + targets := make([]byte, 0) + + err := row.Scan( + &execution.ResourceOwner, + &execution.ID, + &execution.EventDate, + &execution.Sequence, + &targets, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal") + } + + executionTargets := make([]*executionTarget, 0) + if err := json.Unmarshal(targets, &executionTargets); err != nil { + return nil, err + } + + execution.Targets = make([]*exec.Target, len(executionTargets)) + for i := range executionTargets { + if executionTargets[i].Target != "" { + execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: executionTargets[i].Target} + } + if executionTargets[i].Include != "" { + execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: executionTargets[i].Include} + } + } + + return execution, nil +} + +func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) { + executionTargets := make([]*executionTarget, 0) + if err := json.Unmarshal(data, &executionTargets); err != nil { + return nil, err + } + + targets := make([]*exec.Target, len(executionTargets)) + // position starts with 1 + for _, item := range executionTargets { + if item.Target != "" { + targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target} + } + if item.Include != "" { + targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include} + } + } + return targets, nil +} + +func scanExecutions(rows *sql.Rows) (*Executions, error) { + executions := make([]*Execution, 0) + var count uint64 + + for rows.Next() { + execution := new(Execution) + targets := make([]byte, 0) + + err := rows.Scan( + &execution.ResourceOwner, + &execution.ID, + &execution.EventDate, + &execution.Sequence, + &targets, + &count, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-tbrmno85vp", "Errors.Execution.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-tyw2ydsj84", "Errors.Internal") + } + + execution.Targets, err = executionTargetsUnmarshal(targets) + if err != nil { + return nil, err + } + executions = append(executions, execution) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-yhka3fs3mw", "Errors.Query.CloseRows") + } + + return &Executions{ + Executions: executions, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil +} + +type ExecutionTarget struct { + InstanceID string + ExecutionID string + TargetID string + TargetType domain.TargetType + Endpoint string + Timeout time.Duration + InterruptOnError bool +} + +func (e *ExecutionTarget) GetExecutionID() string { + return e.ExecutionID +} +func (e *ExecutionTarget) GetTargetID() string { + return e.TargetID +} +func (e *ExecutionTarget) IsInterruptOnError() bool { + return e.InterruptOnError +} +func (e *ExecutionTarget) GetEndpoint() string { + return e.Endpoint +} +func (e *ExecutionTarget) GetTargetType() domain.TargetType { + return e.TargetType +} +func (e *ExecutionTarget) GetTimeout() time.Duration { + return e.Timeout +} + +func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) { + targets := make([]*ExecutionTarget, 0) + for rows.Next() { + target := new(ExecutionTarget) + + var ( + instanceID = &sql.NullString{} + executionID = &sql.NullString{} + targetID = &sql.NullString{} + targetType = &sql.NullInt32{} + endpoint = &sql.NullString{} + timeout = &sql.NullInt64{} + interruptOnError = &sql.NullBool{} + ) + + err := rows.Scan( + executionID, + instanceID, + targetID, + targetType, + endpoint, + timeout, + interruptOnError, + ) + + if err != nil { + return nil, err + } + + target.InstanceID = instanceID.String + target.ExecutionID = executionID.String + target.TargetID = targetID.String + target.TargetType = domain.TargetType(targetType.Int32) + target.Endpoint = endpoint.String + target.Timeout = time.Duration(timeout.Int64) + target.InterruptOnError = interruptOnError.Bool + + targets = append(targets, target) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-37ardr0pki", "Errors.Query.CloseRows") + } + + return targets, nil } diff --git a/internal/query/execution_targets.sql b/internal/query/execution_targets.sql new file mode 100644 index 0000000000..32257f4a1f --- /dev/null +++ b/internal/query/execution_targets.sql @@ -0,0 +1,11 @@ +SELECT instance_id, + execution_id, + JSONB_AGG( + JSON_OBJECT( + 'position' : position, + 'include' : include, + 'target' : target_id + ) + ) as targets +FROM projections.executions1_targets +GROUP BY instance_id, execution_id \ No newline at end of file diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index 20058b1ae0..b989d539a0 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -8,44 +8,56 @@ import ( "regexp" "testing" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" + exec "github.com/zitadel/zitadel/internal/repository/execution" "github.com/zitadel/zitadel/internal/zerrors" ) var ( - prepareExecutionsStmt = `SELECT projections.executions.id,` + - ` projections.executions.change_date,` + - ` projections.executions.resource_owner,` + - ` projections.executions.sequence,` + - ` projections.executions.targets,` + - ` projections.executions.includes,` + + prepareExecutionsStmt = `SELECT projections.executions1.instance_id,` + + ` projections.executions1.id,` + + ` projections.executions1.change_date,` + + ` projections.executions1.sequence,` + + ` execution_targets.targets,` + ` COUNT(*) OVER ()` + - ` FROM projections.executions` + ` FROM projections.executions1` + + ` JOIN (` + + `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` + + ` FROM projections.executions1_targets` + + ` GROUP BY instance_id, execution_id` + + `)` + + ` AS execution_targets` + + ` ON execution_targets.instance_id = projections.executions1.instance_id` + + ` AND execution_targets.execution_id = projections.executions1.id` prepareExecutionsCols = []string{ + "instance_id", "id", "change_date", - "resource_owner", "sequence", "targets", - "includes", "count", } - prepareExecutionStmt = `SELECT projections.executions.id,` + - ` projections.executions.change_date,` + - ` projections.executions.resource_owner,` + - ` projections.executions.sequence,` + - ` projections.executions.targets,` + - ` projections.executions.includes` + - ` FROM projections.executions` + prepareExecutionStmt = `SELECT projections.executions1.instance_id,` + + ` projections.executions1.id,` + + ` projections.executions1.change_date,` + + ` projections.executions1.sequence,` + + ` execution_targets.targets` + + ` FROM projections.executions1` + + ` JOIN (` + + `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` + + ` FROM projections.executions1_targets` + + ` GROUP BY instance_id, execution_id` + + `)` + + ` AS execution_targets` + + ` ON execution_targets.instance_id = projections.executions1.instance_id` + + ` AND execution_targets.execution_id = projections.executions1.id` prepareExecutionCols = []string{ + "instance_id", "id", "change_date", - "resource_owner", "sequence", "targets", - "includes", } ) @@ -81,12 +93,11 @@ func Test_ExecutionPrepares(t *testing.T) { prepareExecutionsCols, [][]driver.Value{ { + "ro", "id", testNow, - "ro", uint64(20211109), - database.TextArray[string]{"target"}, - database.TextArray[string]{"include"}, + []byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`), }, }, ), @@ -103,8 +114,10 @@ func Test_ExecutionPrepares(t *testing.T) { ResourceOwner: "ro", Sequence: 20211109, }, - Targets: database.TextArray[string]{"target"}, - Includes: database.TextArray[string]{"include"}, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, }, }, }, @@ -118,20 +131,18 @@ func Test_ExecutionPrepares(t *testing.T) { prepareExecutionsCols, [][]driver.Value{ { + "ro", "id-1", testNow, - "ro", uint64(20211109), - database.TextArray[string]{"target1"}, - database.TextArray[string]{"include1"}, + []byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`), }, { + "ro", "id-2", testNow, - "ro", uint64(20211110), - database.TextArray[string]{"target2"}, - database.TextArray[string]{"include2"}, + []byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`), }, }, ), @@ -148,8 +159,10 @@ func Test_ExecutionPrepares(t *testing.T) { ResourceOwner: "ro", Sequence: 20211109, }, - Targets: database.TextArray[string]{"target1"}, - Includes: database.TextArray[string]{"include1"}, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, }, { ID: "id-2", @@ -158,8 +171,10 @@ func Test_ExecutionPrepares(t *testing.T) { ResourceOwner: "ro", Sequence: 20211110, }, - Targets: database.TextArray[string]{"target2"}, - Includes: database.TextArray[string]{"include2"}, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, }, }, @@ -207,12 +222,11 @@ func Test_ExecutionPrepares(t *testing.T) { regexp.QuoteMeta(prepareExecutionStmt), prepareExecutionCols, []driver.Value{ + "ro", "id", testNow, - "ro", uint64(20211109), - database.TextArray[string]{"target"}, - database.TextArray[string]{"include"}, + []byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`), }, ), }, @@ -223,8 +237,10 @@ func Test_ExecutionPrepares(t *testing.T) { ResourceOwner: "ro", Sequence: 20211109, }, - Targets: database.TextArray[string]{"target"}, - Includes: database.TextArray[string]{"include"}, + Targets: []*exec.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + {Type: domain.ExecutionTargetTypeInclude, Target: "include"}, + }, }, }, { diff --git a/internal/query/projection/execution.go b/internal/query/projection/execution.go index 350a27ec27..9001fcd3ba 100644 --- a/internal/query/projection/execution.go +++ b/internal/query/projection/execution.go @@ -3,6 +3,7 @@ package projection import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" @@ -11,15 +12,19 @@ import ( ) const ( - ExecutionTable = "projections.executions" - ExecutionIDCol = "id" - ExecutionCreationDateCol = "creation_date" - ExecutionChangeDateCol = "change_date" - ExecutionResourceOwnerCol = "resource_owner" - ExecutionInstanceIDCol = "instance_id" - ExecutionSequenceCol = "sequence" - ExecutionTargetsCol = "targets" - ExecutionIncludesCol = "includes" + ExecutionTable = "projections.executions1" + ExecutionIDCol = "id" + ExecutionCreationDateCol = "creation_date" + ExecutionChangeDateCol = "change_date" + ExecutionInstanceIDCol = "instance_id" + ExecutionSequenceCol = "sequence" + + ExecutionTargetSuffix = "targets" + ExecutionTargetExecutionIDCol = "execution_id" + ExecutionTargetInstanceIDCol = "instance_id" + ExecutionTargetPositionCol = "position" + ExecutionTargetTargetIDCol = "target_id" + ExecutionTargetIncludeCol = "include" ) type executionProjection struct{} @@ -33,19 +38,28 @@ func (*executionProjection) Name() string { } func (*executionProjection) Init() *old_handler.Check { - return handler.NewTableCheck( + return handler.NewMultiTableCheck( handler.NewTable([]*handler.InitColumn{ handler.NewColumn(ExecutionIDCol, handler.ColumnTypeText), handler.NewColumn(ExecutionCreationDateCol, handler.ColumnTypeTimestamp), handler.NewColumn(ExecutionChangeDateCol, handler.ColumnTypeTimestamp), - handler.NewColumn(ExecutionResourceOwnerCol, handler.ColumnTypeText), - handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText), handler.NewColumn(ExecutionSequenceCol, handler.ColumnTypeInt64), - handler.NewColumn(ExecutionTargetsCol, handler.ColumnTypeTextArray, handler.Nullable()), - handler.NewColumn(ExecutionIncludesCol, handler.ColumnTypeTextArray, handler.Nullable()), + handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText), }, handler.NewPrimaryKey(ExecutionInstanceIDCol, ExecutionIDCol), ), + handler.NewSuffixedTable([]*handler.InitColumn{ + handler.NewColumn(ExecutionTargetInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(ExecutionTargetExecutionIDCol, handler.ColumnTypeText), + handler.NewColumn(ExecutionTargetPositionCol, handler.ColumnTypeInt64), + handler.NewColumn(ExecutionTargetIncludeCol, handler.ColumnTypeText, handler.Nullable()), + handler.NewColumn(ExecutionTargetTargetIDCol, handler.ColumnTypeText, handler.Nullable()), + }, + handler.NewPrimaryKey(ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol, ExecutionTargetPositionCol), + ExecutionTargetSuffix, + handler.WithForeignKey(handler.NewForeignKey("execution", []string{ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol}, []string{ExecutionInstanceIDCol, ExecutionIDCol})), + handler.WithIndex(handler.NewIndex("execution", []string{ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol})), + ), ) } @@ -55,7 +69,7 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer { Aggregate: exec.AggregateType, EventReducers: []handler.EventReducer{ { - Event: exec.SetEventType, + Event: exec.SetEventV2Type, Reduce: p.reduceExecutionSet, }, { @@ -77,21 +91,65 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer { } func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handler.Statement, error) { - e, err := assertEvent[*exec.SetEvent](event) + e, err := assertEvent[*exec.SetEventV2](event) if err != nil { return nil, err } - columns := []handler.Column{ - handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID), - handler.NewCol(ExecutionIDCol, e.Aggregate().ID), - handler.NewCol(ExecutionResourceOwnerCol, e.Aggregate().ResourceOwner), - handler.NewCol(ExecutionCreationDateCol, handler.OnlySetValueOnInsert(ExecutionTable, e.CreationDate())), - handler.NewCol(ExecutionChangeDateCol, e.CreationDate()), - handler.NewCol(ExecutionSequenceCol, e.Sequence()), - handler.NewCol(ExecutionTargetsCol, e.Targets), - handler.NewCol(ExecutionIncludesCol, e.Includes), + + stmts := []func(eventstore.Event) handler.Exec{ + handler.AddUpsertStatement( + []handler.Column{ + handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(ExecutionIDCol, e.Aggregate().ID), + }, + []handler.Column{ + handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(ExecutionIDCol, e.Aggregate().ID), + handler.NewCol(ExecutionCreationDateCol, handler.OnlySetValueOnInsert(ExecutionTable, e.CreationDate())), + handler.NewCol(ExecutionChangeDateCol, e.CreationDate()), + handler.NewCol(ExecutionSequenceCol, e.Sequence()), + }, + ), + // cleanup execution targets to re-insert them + handler.AddDeleteStatement( + []handler.Condition{ + handler.NewCond(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(ExecutionTargetExecutionIDCol, e.Aggregate().ID), + }, + handler.WithTableSuffix(ExecutionTargetSuffix), + ), } - return handler.NewUpsertStatement(e, columns[0:2], columns), nil + + if len(e.Targets) > 0 { + for i, target := range e.Targets { + var targetStr, includeStr string + switch target.Type { + case domain.ExecutionTargetTypeTarget: + targetStr = target.Target + case domain.ExecutionTargetTypeInclude: + includeStr = target.Target + case domain.ExecutionTargetTypeUnspecified: + continue + default: + continue + } + + stmts = append(stmts, + handler.AddCreateStatement( + []handler.Column{ + handler.NewCol(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(ExecutionTargetExecutionIDCol, e.Aggregate().ID), + handler.NewCol(ExecutionTargetPositionCol, i+1), + handler.NewCol(ExecutionTargetIncludeCol, includeStr), + handler.NewCol(ExecutionTargetTargetIDCol, targetStr), + }, + handler.WithTableSuffix(ExecutionTargetSuffix), + ), + ) + } + } + + return handler.NewMultiStatement(e, stmts...), nil } func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) { @@ -99,8 +157,8 @@ func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*h if err != nil { return nil, err } - return handler.NewDeleteStatement( - e, + + return handler.NewDeleteStatement(e, []handler.Condition{ handler.NewCond(ExecutionInstanceIDCol, e.Aggregate().InstanceID), handler.NewCond(ExecutionIDCol, e.Aggregate().ID), diff --git a/internal/query/projection/execution_test.go b/internal/query/projection/execution_test.go index 537e8e0586..27d6e89258 100644 --- a/internal/query/projection/execution_test.go +++ b/internal/query/projection/execution_test.go @@ -25,11 +25,11 @@ func TestExecutionProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - exec.SetEventType, + exec.SetEventV2Type, exec.AggregateType, - []byte(`{"targets": ["target"], "includes": ["include"]}`), + []byte(`{"targets": [{"type":2,"target":"target"},{"type":1,"target":"include"}]}`), ), - eventstore.GenericEventMapper[exec.SetEvent], + eventstore.GenericEventMapper[exec.SetEventV2], ), }, reduce: (&executionProjection{}).reduceExecutionSet, @@ -39,16 +39,40 @@ func TestExecutionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.executions (instance_id, id, resource_owner, creation_date, change_date, sequence, targets, includes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, id) DO UPDATE SET (resource_owner, creation_date, change_date, sequence, targets, includes) = (EXCLUDED.resource_owner, projections.executions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.targets, EXCLUDED.includes)", + expectedStmt: "INSERT INTO projections.executions1 (instance_id, id, creation_date, change_date, sequence) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (instance_id, id) DO UPDATE SET (creation_date, change_date, sequence) = (projections.executions1.creation_date, EXCLUDED.change_date, EXCLUDED.sequence)", expectedArgs: []interface{}{ "instance-id", "agg-id", - "ro-id", anyArg{}, anyArg{}, uint64(15), - []string{"target"}, - []string{"include"}, + }, + }, + { + expectedStmt: "DELETE FROM projections.executions1_targets WHERE (instance_id = $1) AND (execution_id = $2)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + }, + }, + { + expectedStmt: "INSERT INTO projections.executions1_targets (instance_id, execution_id, position, include, target_id) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + 1, + "", + "target", + }, + }, + { + expectedStmt: "INSERT INTO projections.executions1_targets (instance_id, execution_id, position, include, target_id) VALUES ($1, $2, $3, $4, $5)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + 2, + "include", + "", }, }, }, @@ -74,7 +98,7 @@ func TestExecutionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1) AND (id = $2)", + expectedStmt: "DELETE FROM projections.executions1 WHERE (instance_id = $1) AND (id = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", @@ -103,7 +127,7 @@ func TestExecutionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.executions1 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 0f8b2518a6..de7b6135bd 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -269,8 +269,8 @@ func newProjectionsList() { RestrictionsProjection, SystemFeatureProjection, InstanceFeatureProjection, - ExecutionProjection, TargetProjection, + ExecutionProjection, UserSchemaProjection, } } diff --git a/internal/query/projection/target.go b/internal/query/projection/target.go index af801002a9..7b7c46d257 100644 --- a/internal/query/projection/target.go +++ b/internal/query/projection/target.go @@ -11,7 +11,7 @@ import ( ) const ( - TargetTable = "projections.targets" + TargetTable = "projections.targets1" TargetIDCol = "id" TargetCreationDateCol = "creation_date" TargetChangeDateCol = "change_date" @@ -20,9 +20,8 @@ const ( TargetSequenceCol = "sequence" TargetNameCol = "name" TargetTargetType = "target_type" - TargetURLCol = "url" + TargetEndpointCol = "endpoint" TargetTimeoutCol = "timeout" - TargetAsyncCol = "async" TargetInterruptOnErrorCol = "interrupt_on_error" ) @@ -47,10 +46,9 @@ func (*targetProjection) Init() *old_handler.Check { handler.NewColumn(TargetTargetType, handler.ColumnTypeEnum), handler.NewColumn(TargetSequenceCol, handler.ColumnTypeInt64), handler.NewColumn(TargetNameCol, handler.ColumnTypeText), - handler.NewColumn(TargetURLCol, handler.ColumnTypeText, handler.Default("")), - handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64, handler.Default(0)), - handler.NewColumn(TargetAsyncCol, handler.ColumnTypeBool, handler.Default(false)), - handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText), + handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64), + handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool), }, handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol), ), @@ -103,10 +101,9 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S handler.NewCol(TargetChangeDateCol, e.CreationDate()), handler.NewCol(TargetSequenceCol, e.Sequence()), handler.NewCol(TargetNameCol, e.Name), - handler.NewCol(TargetURLCol, e.URL), + handler.NewCol(TargetEndpointCol, e.Endpoint), handler.NewCol(TargetTargetType, e.TargetType), handler.NewCol(TargetTimeoutCol, e.Timeout), - handler.NewCol(TargetAsyncCol, e.Async), handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError), }, ), nil @@ -128,15 +125,12 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler if e.TargetType != nil { values = append(values, handler.NewCol(TargetTargetType, *e.TargetType)) } - if e.URL != nil { - values = append(values, handler.NewCol(TargetURLCol, *e.URL)) + if e.Endpoint != nil { + values = append(values, handler.NewCol(TargetEndpointCol, *e.Endpoint)) } if e.Timeout != nil { values = append(values, handler.NewCol(TargetTimeoutCol, *e.Timeout)) } - if e.Async != nil { - values = append(values, handler.NewCol(TargetAsyncCol, *e.Async)) - } if e.InterruptOnError != nil { values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError)) } diff --git a/internal/query/projection/target_test.go b/internal/query/projection/target_test.go index 1ba0c9379d..30067c6640 100644 --- a/internal/query/projection/target_test.go +++ b/internal/query/projection/target_test.go @@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) { testEvent( target.AddedEventType, target.AggregateType, - []byte(`{"name": "name", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`), + []byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`), ), eventstore.GenericEventMapper[target.AddedEvent], ), @@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.targets (instance_id, resource_owner, id, creation_date, change_date, sequence, name, url, target_type, timeout, async, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + expectedStmt: "INSERT INTO projections.targets1 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", expectedArgs: []interface{}{ "instance-id", "ro-id", @@ -54,7 +54,6 @@ func TestTargetProjection_reduces(t *testing.T) { domain.TargetTypeWebhook, 3 * time.Second, true, - true, }, }, }, @@ -68,7 +67,7 @@ func TestTargetProjection_reduces(t *testing.T) { testEvent( target.ChangedEventType, target.AggregateType, - []byte(`{"name": "name2", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`), + []byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`), ), eventstore.GenericEventMapper[target.ChangedEvent], ), @@ -80,7 +79,7 @@ func TestTargetProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.targets SET (change_date, sequence, resource_owner, name, target_type, url, timeout, async, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)", + expectedStmt: "UPDATE projections.targets1 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (instance_id = $9) AND (id = $10)", expectedArgs: []interface{}{ anyArg{}, uint64(15), @@ -90,7 +89,6 @@ func TestTargetProjection_reduces(t *testing.T) { "https://example.com", 3 * time.Second, true, - true, "instance-id", "agg-id", }, @@ -118,7 +116,7 @@ func TestTargetProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1) AND (id = $2)", + expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1) AND (id = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", @@ -147,7 +145,7 @@ func TestTargetProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, diff --git a/internal/query/search_query.go b/internal/query/search_query.go index b4944a8f2d..868df84fe9 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -478,31 +478,31 @@ func (q *SubSelect) comp() sq.Sqlizer { return selectQuery } -type ListQuery struct { +type listQuery struct { Column Column Data interface{} Compare ListComparison } -func NewListQuery(column Column, value interface{}, compare ListComparison) (*ListQuery, error) { +func NewListQuery(column Column, value interface{}, compare ListComparison) (*listQuery, error) { if compare < 0 || compare >= listCompareMax { return nil, ErrInvalidCompare } if column.isZero() { return nil, ErrMissingColumn } - return &ListQuery{ + return &listQuery{ Column: column, Data: value, Compare: compare, }, nil } -func (q *ListQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { +func (q *listQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query.Where(q.comp()) } -func (q *ListQuery) comp() sq.Sqlizer { +func (q *listQuery) comp() sq.Sqlizer { if q.Compare != ListIn { return nil } @@ -517,7 +517,7 @@ func (q *ListQuery) comp() sq.Sqlizer { return sq.Eq{q.Column.identifier(): q.Data} } -func (q *ListQuery) Col() Column { +func (q *listQuery) Col() Column { return q.Column } @@ -720,6 +720,25 @@ type listContains struct { args interface{} } +func NewListContains(c Column, value interface{}) (*listContains, error) { + return &listContains{ + col: c, + args: value, + }, nil +} + +func (q *listContains) Col() Column { + return q.col +} + +func (q *listContains) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + return query.Where(q.comp()) +} + func (q *listContains) ToSql() (string, []interface{}, error) { return q.col.identifier() + " @> ? ", []interface{}{q.args}, nil } + +func (q *listContains) comp() sq.Sqlizer { + return q +} diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index c64b2c131c..19c1dbcf41 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -521,7 +521,7 @@ func TestNewListQuery(t *testing.T) { tests := []struct { name string args args - want *ListQuery + want *listQuery wantErr func(error) bool }{ { @@ -575,7 +575,7 @@ func TestNewListQuery(t *testing.T) { data: []interface{}{"hurst"}, compare: ListIn, }, - want: &ListQuery{ + want: &listQuery{ Column: testCol, Data: []interface{}{"hurst"}, Compare: ListIn, @@ -588,7 +588,7 @@ func TestNewListQuery(t *testing.T) { data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}}, compare: ListIn, }, - want: &ListQuery{ + want: &listQuery{ Column: testCol, Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}}, Compare: ListIn, @@ -751,7 +751,7 @@ func TestListQuery_comp(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &ListQuery{ + s := &listQuery{ Column: tt.fields.Column, Data: tt.fields.Data, Compare: tt.fields.Compare, diff --git a/internal/query/target.go b/internal/query/target.go index ecb60dca79..c5d8f893ad 100644 --- a/internal/query/target.go +++ b/internal/query/target.go @@ -52,17 +52,13 @@ var ( table: targetTable, } TargetColumnURL = Column{ - name: projection.TargetURLCol, + name: projection.TargetEndpointCol, table: targetTable, } TargetColumnTimeout = Column{ name: projection.TargetTimeoutCol, table: targetTable, } - TargetColumnAsync = Column{ - name: projection.TargetAsyncCol, - table: targetTable, - } TargetColumnInterruptOnError = Column{ name: projection.TargetInterruptOnErrorCol, table: targetTable, @@ -84,9 +80,8 @@ type Target struct { Name string TargetType domain.TargetType - URL string + Endpoint string Timeout time.Duration - Async bool InterruptOnError bool } @@ -138,7 +133,6 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil TargetColumnTargetType.identifier(), TargetColumnTimeout.identifier(), TargetColumnURL.identifier(), - TargetColumnAsync.identifier(), TargetColumnInterruptOnError.identifier(), countColumn.identifier(), ).From(targetTable.identifier()). @@ -156,8 +150,7 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil &target.Name, &target.TargetType, &target.Timeout, - &target.URL, - &target.Async, + &target.Endpoint, &target.InterruptOnError, &count, ) @@ -190,7 +183,6 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild TargetColumnTargetType.identifier(), TargetColumnTimeout.identifier(), TargetColumnURL.identifier(), - TargetColumnAsync.identifier(), TargetColumnInterruptOnError.identifier(), ).From(targetTable.identifier()). PlaceholderFormat(sq.Dollar), @@ -204,8 +196,7 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild &target.Name, &target.TargetType, &target.Timeout, - &target.URL, - &target.Async, + &target.Endpoint, &target.InterruptOnError, ) if err != nil { diff --git a/internal/query/target_test.go b/internal/query/target_test.go index d1003fc8d8..61a61e9e45 100644 --- a/internal/query/target_test.go +++ b/internal/query/target_test.go @@ -14,18 +14,17 @@ import ( ) var ( - prepareTargetsStmt = `SELECT projections.targets.id,` + - ` projections.targets.change_date,` + - ` projections.targets.resource_owner,` + - ` projections.targets.sequence,` + - ` projections.targets.name,` + - ` projections.targets.target_type,` + - ` projections.targets.timeout,` + - ` projections.targets.url,` + - ` projections.targets.async,` + - ` projections.targets.interrupt_on_error,` + + prepareTargetsStmt = `SELECT projections.targets1.id,` + + ` projections.targets1.change_date,` + + ` projections.targets1.resource_owner,` + + ` projections.targets1.sequence,` + + ` projections.targets1.name,` + + ` projections.targets1.target_type,` + + ` projections.targets1.timeout,` + + ` projections.targets1.endpoint,` + + ` projections.targets1.interrupt_on_error,` + ` COUNT(*) OVER ()` + - ` FROM projections.targets` + ` FROM projections.targets1` prepareTargetsCols = []string{ "id", "change_date", @@ -34,23 +33,21 @@ var ( "name", "target_type", "timeout", - "url", - "async", + "endpoint", "interrupt_on_error", "count", } - prepareTargetStmt = `SELECT projections.targets.id,` + - ` projections.targets.change_date,` + - ` projections.targets.resource_owner,` + - ` projections.targets.sequence,` + - ` projections.targets.name,` + - ` projections.targets.target_type,` + - ` projections.targets.timeout,` + - ` projections.targets.url,` + - ` projections.targets.async,` + - ` projections.targets.interrupt_on_error` + - ` FROM projections.targets` + prepareTargetStmt = `SELECT projections.targets1.id,` + + ` projections.targets1.change_date,` + + ` projections.targets1.resource_owner,` + + ` projections.targets1.sequence,` + + ` projections.targets1.name,` + + ` projections.targets1.target_type,` + + ` projections.targets1.timeout,` + + ` projections.targets1.endpoint,` + + ` projections.targets1.interrupt_on_error` + + ` FROM projections.targets1` prepareTargetCols = []string{ "id", "change_date", @@ -59,8 +56,7 @@ var ( "name", "target_type", "timeout", - "url", - "async", + "endpoint", "interrupt_on_error", } ) @@ -106,7 +102,6 @@ func Test_TargetPrepares(t *testing.T) { 1 * time.Second, "https://example.com", true, - true, }, }, ), @@ -126,8 +121,7 @@ func Test_TargetPrepares(t *testing.T) { Name: "target-name", TargetType: domain.TargetTypeWebhook, Timeout: 1 * time.Second, - URL: "https://example.com", - Async: true, + Endpoint: "https://example.com", InterruptOnError: true, }, }, @@ -151,7 +145,6 @@ func Test_TargetPrepares(t *testing.T) { 1 * time.Second, "https://example.com", true, - false, }, { "id-2", @@ -163,14 +156,24 @@ func Test_TargetPrepares(t *testing.T) { 1 * time.Second, "https://example.com", false, - true, + }, + { + "id-3", + testNow, + "ro", + uint64(20211110), + "target-name3", + domain.TargetTypeAsync, + 1 * time.Second, + "https://example.com", + false, }, }, ), }, object: &Targets{ SearchResponse: SearchResponse{ - Count: 2, + Count: 3, }, Targets: []*Target{ { @@ -183,9 +186,8 @@ func Test_TargetPrepares(t *testing.T) { Name: "target-name1", TargetType: domain.TargetTypeWebhook, Timeout: 1 * time.Second, - URL: "https://example.com", - Async: true, - InterruptOnError: false, + Endpoint: "https://example.com", + InterruptOnError: true, }, { ID: "id-2", @@ -197,9 +199,21 @@ func Test_TargetPrepares(t *testing.T) { Name: "target-name2", TargetType: domain.TargetTypeWebhook, Timeout: 1 * time.Second, - URL: "https://example.com", - Async: false, - InterruptOnError: true, + Endpoint: "https://example.com", + InterruptOnError: false, + }, + { + ID: "id-3", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211110, + }, + Name: "target-name3", + TargetType: domain.TargetTypeAsync, + Timeout: 1 * time.Second, + Endpoint: "https://example.com", + InterruptOnError: false, }, }, }, @@ -256,7 +270,6 @@ func Test_TargetPrepares(t *testing.T) { 1 * time.Second, "https://example.com", true, - false, }, ), }, @@ -270,9 +283,8 @@ func Test_TargetPrepares(t *testing.T) { Name: "target-name", TargetType: domain.TargetTypeWebhook, Timeout: 1 * time.Second, - URL: "https://example.com", - Async: true, - InterruptOnError: false, + Endpoint: "https://example.com", + InterruptOnError: true, }, }, { diff --git a/internal/query/targets_by_execution_id.sql b/internal/query/targets_by_execution_id.sql new file mode 100644 index 0000000000..6b564104e5 --- /dev/null +++ b/internal/query/targets_by_execution_id.sql @@ -0,0 +1,40 @@ +WITH RECURSIVE + dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id") + AS (SELECT execution_id + , instance_id + , ARRAY [position] + , "include" + , "target_id" + FROM matched_targets_and_includes + UNION ALL + SELECT e.execution_id + , p.instance_id + , e.position || p.position + , p."include" + , p."target_id" + FROM dissolved_execution_targets e + JOIN projections.executions1_targets p + ON e.instance_id = p.instance_id + AND e.include IS NOT NULL + AND e.include = p.execution_id), + matched AS (SELECT * + FROM projections.executions1 + WHERE instance_id = $1 + AND id = ANY($2) + ORDER BY id DESC + LIMIT 1), + matched_targets_and_includes AS (SELECT pos.* + FROM matched m + JOIN + projections.executions1_targets pos + ON m.id = pos.execution_id + AND m.instance_id = pos.instance_id + ORDER BY execution_id, + position) +select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error +FROM dissolved_execution_targets e + JOIN projections.targets1 t + ON e.instance_id = t.instance_id + AND e.target_id = t.id +WHERE "include" = '' +ORDER BY position DESC; diff --git a/internal/query/targets_by_execution_ids.sql b/internal/query/targets_by_execution_ids.sql new file mode 100644 index 0000000000..c1361d9320 --- /dev/null +++ b/internal/query/targets_by_execution_ids.sql @@ -0,0 +1,47 @@ +WITH RECURSIVE + dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id") + AS (SELECT execution_id + , instance_id + , ARRAY [position] + , "include" + , "target_id" + FROM matched_targets_and_includes + UNION ALL + SELECT e.execution_id + , p.instance_id + , e.position || p.position + , p."include" + , p."target_id" + FROM dissolved_execution_targets e + JOIN projections.executions1_targets p + ON e.instance_id = p.instance_id + AND e.include IS NOT NULL + AND e.include = p.execution_id), + matched AS ((SELECT * + FROM projections.executions1 + WHERE instance_id = $1 + AND id = ANY($2) + ORDER BY id DESC + LIMIT 1) + UNION ALL + (SELECT * + FROM projections.executions1 + WHERE instance_id = $1 + AND id = ANY($3) + ORDER BY id DESC + LIMIT 1)), + matched_targets_and_includes AS (SELECT pos.* + FROM matched m + JOIN + projections.executions1_targets pos + ON m.id = pos.execution_id + AND m.instance_id = pos.instance_id + ORDER BY execution_id, + position) +select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error +FROM dissolved_execution_targets e + JOIN projections.targets1 t + ON e.instance_id = t.instance_id + AND e.target_id = t.id +WHERE "include" = '' +ORDER BY position DESC; diff --git a/internal/repository/execution/aggregate.go b/internal/repository/execution/aggregate.go index 973bb17854..eea14f8468 100644 --- a/internal/repository/execution/aggregate.go +++ b/internal/repository/execution/aggregate.go @@ -23,7 +23,10 @@ func NewAggregate(aggrID, instanceID string) *eventstore.Aggregate { } func ID(executionType domain.ExecutionType, value string) string { - return strings.Join([]string{executionType.String(), value}, ".") + if strings.HasPrefix(value, "/") { + return strings.Join([]string{executionType.String(), value}, "") + } + return strings.Join([]string{executionType.String(), value}, "/") } func IDAll(executionType domain.ExecutionType) string { diff --git a/internal/repository/execution/eventstore.go b/internal/repository/execution/eventstore.go index bf4e7bc79f..2d93000217 100644 --- a/internal/repository/execution/eventstore.go +++ b/internal/repository/execution/eventstore.go @@ -4,5 +4,6 @@ import "github.com/zitadel/zitadel/internal/eventstore" func init() { eventstore.RegisterFilterEventMapper(AggregateType, SetEventType, eventstore.GenericEventMapper[SetEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, SetEventV2Type, eventstore.GenericEventMapper[SetEventV2]) eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent]) } diff --git a/internal/repository/execution/execution.go b/internal/repository/execution/execution.go index 4c1a85a6b6..a6851b4495 100644 --- a/internal/repository/execution/execution.go +++ b/internal/repository/execution/execution.go @@ -3,12 +3,14 @@ package execution import ( "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" ) const ( eventTypePrefix eventstore.EventType = "execution." SetEventType = eventTypePrefix + "set" + SetEventV2Type = eventTypePrefix + "v2.set" RemovedEventType = eventTypePrefix + "removed" ) @@ -31,18 +33,39 @@ func (e *SetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { return nil } -func NewSetEvent( +type SetEventV2 struct { + *eventstore.BaseEvent `json:"-"` + + Targets []*Target `json:"targets"` +} + +func (e *SetEventV2) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *SetEventV2) Payload() any { + return e +} + +func (e *SetEventV2) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +type Target struct { + Type domain.ExecutionTargetType `json:"type"` + Target string `json:"target"` +} + +func NewSetEventV2( ctx context.Context, aggregate *eventstore.Aggregate, - targets []string, - includes []string, -) *SetEvent { - return &SetEvent{ + targets []*Target, +) *SetEventV2 { + return &SetEventV2{ BaseEvent: eventstore.NewBaseEventForPush( - ctx, aggregate, SetEventType, + ctx, aggregate, SetEventV2Type, ), - Targets: targets, - Includes: includes, + Targets: targets, } } diff --git a/internal/repository/target/target.go b/internal/repository/target/target.go index 2d50857cba..85e3ae7023 100644 --- a/internal/repository/target/target.go +++ b/internal/repository/target/target.go @@ -20,9 +20,8 @@ type AddedEvent struct { Name string `json:"name"` TargetType domain.TargetType `json:"targetType"` - URL string `json:"url"` + Endpoint string `json:"endpoint"` Timeout time.Duration `json:"timeout"` - Async bool `json:"async"` InterruptOnError bool `json:"interruptOnError"` } @@ -43,16 +42,15 @@ func NewAddedEvent( aggregate *eventstore.Aggregate, name string, targetType domain.TargetType, - url string, + endpoint string, timeout time.Duration, - async bool, interruptOnError bool, ) *AddedEvent { return &AddedEvent{ *eventstore.NewBaseEventForPush( ctx, aggregate, AddedEventType, ), - name, targetType, url, timeout, async, interruptOnError} + name, targetType, endpoint, timeout, interruptOnError} } type ChangedEvent struct { @@ -60,9 +58,8 @@ type ChangedEvent struct { Name *string `json:"name,omitempty"` TargetType *domain.TargetType `json:"targetType,omitempty"` - URL *string `json:"url,omitempty"` + Endpoint *string `json:"endpoint,omitempty"` Timeout *time.Duration `json:"timeout,omitempty"` - Async *bool `json:"async,omitempty"` InterruptOnError *bool `json:"interruptOnError,omitempty"` oldName string @@ -119,9 +116,9 @@ func ChangeTargetType(targetType domain.TargetType) func(event *ChangedEvent) { } } -func ChangeURL(url string) func(event *ChangedEvent) { +func ChangeEndpoint(endpoint string) func(event *ChangedEvent) { return func(e *ChangedEvent) { - e.URL = &url + e.Endpoint = &endpoint } } @@ -131,12 +128,6 @@ func ChangeTimeout(timeout time.Duration) func(event *ChangedEvent) { } } -func ChangeAsync(async bool) func(event *ChangedEvent) { - return func(e *ChangedEvent) { - e.Async = &async - } -} - func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) { return func(e *ChangedEvent) { e.InterruptOnError = &interruptOnError diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 2caea6096a..879c4a3a9c 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -485,6 +485,7 @@ Errors: NotActive: Действието не е активно NotInactive: Действието не е неактивно MaxAllowed: Не са разрешени допълнителни активни действия + NotEnabled: Функцията „Действие“ не е активирана Flow: FlowTypeMissing: Липсва FlowType Empty: Потокът вече е празен diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index cef11e3b20..ac79d10565 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -471,6 +471,7 @@ Errors: NotActive: Akce není aktivní NotInactive: Akce není neaktivní MaxAllowed: Není dovoleno více aktivních akcí + NotEnabled: Funkce "Akce" není povolena Flow: FlowTypeMissing: Chybí typ toku Empty: Tok je již prázdný diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 8c3d41486d..b0a58884c6 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -471,6 +471,7 @@ Errors: NotActive: Action ist nicht aktiv NotInactive: Action ist nicht inaktiv MaxAllowed: Keine weitere aktiven Actions mehr erlaubt + NotEnabled: Function "Action" ist nicht aktiviert Flow: FlowTypeMissing: FlowType fehlt Empty: Flow ist bereits leer diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 81849126ff..c2ec0f0fff 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -471,6 +471,7 @@ Errors: NotActive: Action is not active NotInactive: Action is not inactive MaxAllowed: No additional active Actions allowed + NotEnabled: Feature "Action" is not enabled Flow: FlowTypeMissing: FlowType missing Empty: Flow is already empty diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 0326d6158d..d8194d921b 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -471,6 +471,7 @@ Errors: NotActive: La acción no está activa NotInactive: La acción no está inactiva MaxAllowed: No hay acciones adicionales activas permitidas + NotEnabled: La función "Acción" no está habilitada Flow: FlowTypeMissing: Falta el tipo de flujo Empty: El flujo ya está vacío diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 5f332f26bb..4b278c92b4 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -471,6 +471,7 @@ Errors: NotActive: L'action n'est pas active NotInactive: L'action n'est pas inactive MaxAllowed: Aucune action active supplémentaire n'est autorisée + NotEnabled: La fonctionnalité "Action" n'est pas activée Flow: FlowTypeMissing: FlowType missing Empty: Le flux est déjà vide diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index fa58e1ae39..3474d914f8 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -471,6 +471,7 @@ Errors: NotActive: L'azione non è attiva NotInactive: L'azione non è inattiva MaxAllowed: Non sono permesse altre azioni attive + NotEnabled: La funzione "Azione" non è abilitata Flow: FlowTypeMissing: FlowType mancante Empty: Flow è già vuoto diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 13f3655140..d19f16053c 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -460,6 +460,7 @@ Errors: NotActive: アクションはアクティブではありません NotInactive: アクションは非アクティブではありません MaxAllowed: 追加のアクティブアクションは許可されていません + NotEnabled: 機能「アクション」が有効になっていません Flow: FlowTypeMissing: フロータイプがありません Empty: フローはすでに空です diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index d58509fe5c..11b64d3ede 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -470,6 +470,7 @@ Errors: NotActive: Акцијата не е активна NotInactive: Акцијата не е неактивна MaxAllowed: Не се дозволени дополнителни активни акции + NotEnabled: Функцијата „Акција“ не е овозможена Flow: FlowTypeMissing: FlowType не е наведен Empty: Flow е веќе празен diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index c815f50362..ad5b6561df 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -470,6 +470,7 @@ Errors: NotActive: Actie is niet actief NotInactive: Actie is niet inactief MaxAllowed: Geen extra actieve acties toegestaan + NotEnabled: Functie "Actie" is niet ingeschakeld Flow: FlowTypeMissing: FlowType ontbreekt Empty: Flow is al leeg diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index bcbb7c69f9..129f0ea073 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -471,6 +471,7 @@ Errors: NotActive: Działanie nie jest aktywne NotInactive: Działanie nie jest dezaktywowane MaxAllowed: Nie dopuszcza się dodatkowych aktywnych działań. + NotEnabled: Funkcja „Akcja” nie jest włączona Flow: FlowTypeMissing: Typ przepływu brakuje Empty: Przepływ jest już pusty diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index e3548d2096..e7f8858f20 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -470,6 +470,7 @@ Errors: NotActive: A ação não está ativa NotInactive: A ação não está inativa MaxAllowed: Não são permitidas ações adicionais ativas + NotEnabled: O recurso "Ação" não está ativado Flow: FlowTypeMissing: O tipo de fluxo está faltando Empty: O fluxo já está vazio diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index e7985ac853..7e530ce8b7 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -464,6 +464,7 @@ Errors: NotActive: Действие не активно NotInactive: Действие не является неактивным MaxAllowed: Дополнительные активные действия запрещены + NotEnabled: Функция «Действие» не включена Flow: FlowTypeMissing: Тип процесса отсутствует Empty: Процесс уже пуст diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index a13a02a202..0fae191b67 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -471,6 +471,7 @@ Errors: NotActive: 动作不是启用状态 NotInactive: 动作不是停用状态 MaxAllowed: 不允许额外的动作 + NotEnabled: 未启用“操作”功能 Flow: FlowTypeMissing: 缺少身份认证流程类型 Empty: 身份认证流程为空 diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go index 329d533631..bb13d28bd9 100644 --- a/internal/webauthn/webauthn.go +++ b/internal/webauthn/webauthn.go @@ -95,7 +95,7 @@ func (w *Config) BeginRegistration(ctx context.Context, user *domain.Human, acco }, nil } -func (w *Config) FinishRegistration(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNToken, tokenName string, credData []byte, isLoginUI bool) (*domain.WebAuthNToken, error) { +func (w *Config) FinishRegistration(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNToken, tokenName string, credData []byte) (*domain.WebAuthNToken, error) { if webAuthN == nil { return nil, zerrors.ThrowInternal(nil, "WEBAU-5M9so", "Errors.User.WebAuthN.NotFound") } diff --git a/proto/zitadel/action/v3alpha/action_service.proto b/proto/zitadel/action/v3alpha/action_service.proto index 4c37276326..da174b45d0 100644 --- a/proto/zitadel/action/v3alpha/action_service.proto +++ b/proto/zitadel/action/v3alpha/action_service.proto @@ -423,22 +423,26 @@ message CreateTargetRequest { option (validate.required) = true; SetRESTWebhook rest_webhook = 2; - SetRESTRequestResponse rest_request_response = 3; + SetRESTCall rest_call = 3; + SetRESTAsync rest_async = 4; } // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 4 [ + google.protobuf.Duration timeout = 5 [ (validate.rules).duration = {gt: {seconds: 0}, required: true}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"10s\""; } ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 5; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 6; - } + string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"https://example.com/hooks/ip_check\""; + } + ]; } message CreateTargetResponse { @@ -472,21 +476,24 @@ message UpdateTargetRequest { // or its target URL. oneof target_type { SetRESTWebhook rest_webhook = 3; - SetRESTRequestResponse rest_request_response = 4; + SetRESTCall rest_call = 4; + SetRESTAsync rest_async = 5; } // Optionally change the timeout, which defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 5 [ + optional google.protobuf.Duration timeout = 6 [ (validate.rules).duration = {gt: {seconds: 0}}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"10s\""; } ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 6; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 7; - } + + optional string endpoint = 7 [ + (validate.rules).string = {max_len: 1000, uri: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 1000, + example: "\"https://example.com/hooks/ip_check\""; + } + ]; } message UpdateTargetResponse { @@ -554,10 +561,8 @@ message GetTargetByIDResponse { message SetExecutionRequest { // Defines the condition type and content of the condition for execution. Condition condition = 1; - // Defines the execution targets which are defined as a different resource, which are called in the defined conditions. - repeated string targets = 2; - // Defines other executions as included with the same condition-types. - repeated string includes = 3; + // Ordered list of targets/includes called during the execution. + repeated zitadel.action.v3alpha.ExecutionTargetType targets = 2; } message SetExecutionResponse { diff --git a/proto/zitadel/action/v3alpha/execution.proto b/proto/zitadel/action/v3alpha/execution.proto index c8b85f50a0..6f24471185 100644 --- a/proto/zitadel/action/v3alpha/execution.proto +++ b/proto/zitadel/action/v3alpha/execution.proto @@ -14,17 +14,20 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; message Execution { - string execution_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"request.zitadel.session.v2beta.SessionService\""; - } - ]; + Condition Condition = 1; // Details provide some base information (such as the last change date) of the target. zitadel.object.v2beta.Details details = 2; - // Targets which are called in the defined conditions. - repeated string targets = 3; - // Included executions with the same condition-types. - repeated string includes = 4; + // List of ordered list of targets/includes called during the execution. + repeated ExecutionTargetType targets = 3; +} + +message ExecutionTargetType { + oneof type { + // Unique identifier of existing target to call. + string target = 1; + // Unique identifier of existing execution to include targets of. + Condition include = 2; + } } message Condition { @@ -37,7 +40,7 @@ message Condition { // Condition-type to execute on response if a request on the defined API point happens. ResponseExecution response = 2; // Condition-type to execute if function is used, replaces actions v1. - string function = 3; + FunctionExecution function = 3; // Condition-type to execute if an event is created in the system. EventExecution event = 4; } @@ -95,6 +98,11 @@ message ResponseExecution { } } +// Executed on the specified function +message FunctionExecution { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1000}]; +} + message EventExecution{ // Condition for the event execution, only one possible. oneof condition{ diff --git a/proto/zitadel/action/v3alpha/query.proto b/proto/zitadel/action/v3alpha/query.proto index 26093305bc..a32caacfba 100644 --- a/proto/zitadel/action/v3alpha/query.proto +++ b/proto/zitadel/action/v3alpha/query.proto @@ -43,7 +43,7 @@ message TargetQuery { message IncludeQuery { // Defines the include to query for. - string include = 1 [ + Condition include = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "the id of the include" example: "\"request.zitadel.session.v2beta.SessionService\""; diff --git a/proto/zitadel/action/v3alpha/target.proto b/proto/zitadel/action/v3alpha/target.proto index a034c58ace..92dda32bbb 100644 --- a/proto/zitadel/action/v3alpha/target.proto +++ b/proto/zitadel/action/v3alpha/target.proto @@ -13,30 +13,21 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; +// Wait for response but response body is ignored, status is checked, call is sent as post. message SetRESTWebhook { - string url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; } -message SetRESTRequestResponse { - string url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; +// Wait for response and response body is used, status is checked, call is sent as post. +message SetRESTCall { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; } +// Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. +message SetRESTAsync {} + message Target { // ID is the read-only unique identifier of the target. string target_id = 1 [ @@ -56,18 +47,19 @@ message Target { // Defines the target type and how the response of the target is treated. oneof target_type { SetRESTWebhook rest_webhook = 4; - SetRESTRequestResponse rest_request_response = 5; + SetRESTCall rest_call = 5; + SetRESTAsync rest_async = 6; } // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 6 [ + google.protobuf.Duration timeout = 7 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"10s\""; } ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 7; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 8; - } + + string endpoint = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\""; + } + ]; } \ No newline at end of file diff --git a/release-channels.yaml b/release-channels.yaml index ec2fad5cd3..d8b29a5ab7 100644 --- a/release-channels.yaml +++ b/release-channels.yaml @@ -1 +1 @@ -stable: "v2.45.6" +stable: "v2.46.7"