feat(v3alpha): read actions (#8357)

# Which Problems Are Solved

The current v3alpha actions APIs don't exactly adhere to the [new
resources API
design](https://zitadel.com/docs/apis/v3#standard-resources).

# How the Problems Are Solved

- **Improved ID access**: The aggregate ID is added to the resource
details object, so accessing resource IDs and constructing proto
messages for resources is easier
- **Explicit Instances**: Optionally, the instance can be explicitly
given in each request
- **Pagination**: A default search limit and a max search limit are
added to the defaults.yaml. They apply to the new v3 APIs (currently
only actions). The search query defaults are changed to ascending by
creation date, because this makes the pagination results the most
deterministic. The creation date is also added to the object details.
The bug with updated creation dates is fixed for executions and targets.
- **Removed Sequences**: Removed Sequence from object details and
ProcessedSequence from search details

# Additional Changes

Object details IDs are checked in unit test only if an empty ID is
expected. Centralizing the details check also makes this internal object
more flexible for future evolutions.

# Additional Context

- Closes #8169 
- Depends on https://github.com/zitadel/zitadel/pull/8225

---------

Co-authored-by: Silvan <silvan.reusser@gmail.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Elio Bischof
2024-08-12 22:32:01 +02:00
committed by GitHub
parent 18c3f574a9
commit 042c438813
130 changed files with 3253 additions and 605 deletions

View File

@@ -29,7 +29,7 @@ type Instance interface {
type InstanceVerifier interface {
InstanceByHost(ctx context.Context, host, publicDomain string) (Instance, error)
InstanceByID(ctx context.Context) (Instance, error)
InstanceByID(ctx context.Context, id string) (Instance, error)
}
type instance struct {

View File

@@ -4,7 +4,7 @@ import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
settings_object "github.com/zitadel/zitadel/internal/api/grpc/settings/object/v3alpha"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
@@ -14,7 +14,7 @@ import (
)
func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
reqTargets := req.GetExecution().GetTargets()
@@ -34,24 +34,21 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
set := &command.SetExecution{
Targets: targets,
}
owner := &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: authz.GetInstance(ctx).InstanceID(),
}
var err error
var details *domain.ObjectDetails
instanceID := authz.GetInstance(ctx).InstanceID()
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, owner.Id)
details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID)
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, owner.Id)
details, err = s.command.SetExecutionResponse(ctx, cond, set, instanceID)
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, owner.Id)
details, err = s.command.SetExecutionEvent(ctx, cond, set, instanceID)
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, owner.Id)
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, instanceID)
default:
err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid")
}
@@ -59,7 +56,7 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
return nil, err
}
return &action.SetExecutionResponse{
Details: settings_object.DomainToDetailsPb(details, owner),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
@@ -25,8 +25,9 @@ func executionTargetsSingleInclude(include *action.Condition) []*action.Executio
}
func TestServer_SetExecution_Request(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -51,7 +52,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
{
name: "no condition, error",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -66,7 +67,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
{
name: "method, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -85,7 +86,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
{
name: "method, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -101,18 +102,18 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
},
{
name: "service, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -131,7 +132,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
{
name: "service, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -147,18 +148,18 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
},
{
name: "all, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -174,11 +175,11 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -187,8 +188,8 @@ func TestServer_SetExecution_Request(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
@@ -196,7 +197,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -205,8 +206,9 @@ func TestServer_SetExecution_Request(t *testing.T) {
}
func TestServer_SetExecution_Request_Include(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
executionCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
@@ -216,7 +218,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
}
Tester.SetExecution(CTX, t,
Tester.SetExecution(isolatedIAMOwnerCTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
)
@@ -230,7 +232,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
}
Tester.SetExecution(CTX, t,
Tester.SetExecution(isolatedIAMOwnerCTX, t,
circularExecutionService,
executionTargetsSingleInclude(executionCond),
)
@@ -243,7 +245,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
}
Tester.SetExecution(CTX, t,
Tester.SetExecution(isolatedIAMOwnerCTX, t,
circularExecutionMethod,
executionTargetsSingleInclude(circularExecutionService),
)
@@ -257,7 +259,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
}{
{
name: "method, circular error",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: circularExecutionService,
Execution: &action.Execution{
@@ -268,7 +270,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
{
name: "method, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -280,23 +282,22 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
},
{
name: "service, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
@@ -308,16 +309,15 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -326,15 +326,15 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -343,8 +343,9 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
}
func TestServer_SetExecution_Response(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -369,7 +370,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
{
name: "no condition, error",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -384,7 +385,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
{
name: "method, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -403,7 +404,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
{
name: "method, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -419,18 +420,18 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
},
{
name: "service, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -449,7 +450,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
{
name: "service, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -465,18 +466,18 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
},
{
name: "all, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -492,11 +493,11 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -505,15 +506,15 @@ func TestServer_SetExecution_Response(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -522,8 +523,9 @@ func TestServer_SetExecution_Response(t *testing.T) {
}
func TestServer_SetExecution_Event(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -550,7 +552,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
{
name: "no condition, error",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -568,7 +570,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
{
name: "event, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -586,7 +588,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
*/
{
name: "event, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -602,11 +604,11 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -616,7 +618,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
{
name: "group, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -634,7 +636,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
*/
{
name: "group, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -650,18 +652,18 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
},
{
name: "all, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
@@ -677,11 +679,11 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -690,15 +692,15 @@ func TestServer_SetExecution_Event(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
@@ -707,8 +709,9 @@ func TestServer_SetExecution_Event(t *testing.T) {
}
func TestServer_SetExecution_Function(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -733,7 +736,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
},
{
name: "no condition, error",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
@@ -748,7 +751,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
},
{
name: "function, not existing",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
@@ -763,7 +766,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
},
{
name: "function, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
@@ -775,11 +778,11 @@ func TestServer_SetExecution_Function(t *testing.T) {
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -788,15 +791,15 @@ func TestServer_SetExecution_Function(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())

View File

@@ -0,0 +1,335 @@
//go:build integration
package action_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"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"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_ExecutionTarget(t *testing.T) {
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget"
tests := []struct {
name string
ctx context.Context
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (func(), error)
clean func(context.Context)
req *action.GetTargetRequest
want *action.GetTargetResponse
wantErr bool
}{
{
name: "GetTarget, request and response, ok",
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) {
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(instanceID, integration.IAMOwner).ID
// create target for target changes
targetCreatedName := fmt.Sprint("GetTarget", 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.GetTargetRequest{Id: targetCreated.GetDetails().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.GetDetails().GetId()))
// expected response from the GetTarget
expectedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Config: &action.Target{
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
Details: targetCreated.GetDetails(),
},
}
// has to be set separately because of the pointers
response.Target = &action.GetTarget{
Details: targetCreated.GetDetails(),
Config: &action.Target{
Name: targetCreatedName,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
Endpoint: targetCreatedURL,
},
}
// content for partial update
changedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Id: targetCreated.GetDetails().GetId(),
},
},
}
// 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.GetDetails().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.GetTargetRequest{
Id: "something",
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Id: "changed",
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instanceID,
},
},
},
},
},
{
name: "GetTarget, request, interrupt",
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) {
fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget"
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(instanceID, 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.GetTargetRequest{Id: "notchanged"})
targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, true)
Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetDetails().GetId()))
// GetTarget with used target
request.Id = targetRequest.GetDetails().GetId()
return func() {
closeRequest()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
},
req: &action.GetTargetRequest{},
wantErr: true,
},
{
name: "GetTarget, response, interrupt",
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), error) {
fullMethod := "/zitadel.resources.action.v3alpha.ZITADELActions/GetTarget"
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(instanceID, integration.IAMOwner).ID
// create target for target changes
targetCreatedName := fmt.Sprint("GetTarget", time.Now().UnixNano()+1)
targetCreatedURL := "https://nonexistent"
targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false)
// GetTarget with used target
request.Id = targetCreated.GetDetails().GetId()
// expected response from the GetTarget
expectedResponse := &action.GetTargetResponse{
Target: &action.GetTarget{
Details: targetCreated.GetDetails(),
Config: &action.Target{
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.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Id: "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.GetDetails().GetId()))
return func() {
closeResponse()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetRequest{},
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 := Tester.Client.ActionV3.GetTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.GetTarget().GetDetails(), got.GetTarget().GetDetails())
require.Equal(t, tt.want.GetTarget().GetConfig(), got.GetTarget().GetConfig())
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
}

View File

@@ -0,0 +1,410 @@
package action
import (
"context"
"strings"
"google.golang.org/protobuf/types/known/durationpb"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
const (
conditionIDAllSegmentCount = 0
conditionIDRequestResponseServiceSegmentCount = 1
conditionIDRequestResponseMethodSegmentCount = 2
conditionIDEventGroupSegmentCount = 1
)
func (s *Server) GetTarget(ctx context.Context, req *action.GetTargetRequest) (*action.GetTargetResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
resp, err := s.query.GetTargetByID(ctx, req.GetId())
if err != nil {
return nil, err
}
return &action.GetTargetResponse{
Target: targetToPb(resp),
}, nil
}
type InstanceContext interface {
GetInstanceId() string
GetInstanceDomain() string
}
type Context interface {
GetOwner() InstanceContext
}
func (s *Server) SearchTargets(ctx context.Context, req *action.SearchTargetsRequest) (*action.SearchTargetsResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
queries, err := s.searchTargetsRequestToModel(req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchTargets(ctx, queries)
if err != nil {
return nil, err
}
return &action.SearchTargetsResponse{
Result: targetsToPb(resp.Targets),
Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse),
}, nil
}
func (s *Server) SearchExecutions(ctx context.Context, req *action.SearchExecutionsRequest) (*action.SearchExecutionsResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
queries, err := s.searchExecutionsRequestToModel(req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchExecutions(ctx, queries)
if err != nil {
return nil, err
}
return &action.SearchExecutionsResponse{
Result: executionsToPb(resp.Executions),
Details: resource_object.ToSearchDetailsPb(queries.SearchRequest, resp.SearchResponse),
}, nil
}
func targetsToPb(targets []*query.Target) []*action.GetTarget {
t := make([]*action.GetTarget, len(targets))
for i, target := range targets {
t[i] = targetToPb(target)
}
return t
}
func targetToPb(t *query.Target) *action.GetTarget {
target := &action.GetTarget{
Details: resource_object.DomainToDetailsPb(&t.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, t.ResourceOwner),
Config: &action.Target{
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
},
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.Config.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeCall:
target.Config.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeAsync:
target.Config.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}}
default:
target.Config.TargetType = nil
}
return target
}
func (s *Server) searchTargetsRequestToModel(req *action.SearchTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query)
if err != nil {
return nil, err
}
queries, err := targetQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.TargetSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func targetQueriesToQuery(queries []*action.TargetSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, qry := range queries {
q[i], err = targetQueryToQuery(qry)
if err != nil {
return nil, err
}
}
return q, nil
}
func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, error) {
switch q := filter.Filter.(type) {
case *action.TargetSearchFilter_TargetNameFilter:
return targetNameQueryToQuery(q.TargetNameFilter)
case *action.TargetSearchFilter_InTargetIdsFilter:
return targetInTargetIdsQueryToQuery(q.InTargetIdsFilter)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) {
return query.NewTargetNameSearchQuery(resource_object.TextMethodPbToQuery(q.Method), q.GetTargetName())
}
func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) {
return query.NewTargetInIDsSearchQuery(q.GetTargetIds())
}
// targetFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column {
if field == nil {
return query.TargetColumnCreationDate
}
switch *field {
case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED:
return query.TargetColumnID
case action.TargetFieldName_TARGET_FIELD_NAME_ID:
return query.TargetColumnID
case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE:
return query.TargetColumnCreationDate
case action.TargetFieldName_TARGET_FIELD_NAME_CHANGED_DATE:
return query.TargetColumnChangeDate
case action.TargetFieldName_TARGET_FIELD_NAME_NAME:
return query.TargetColumnName
case action.TargetFieldName_TARGET_FIELD_NAME_TARGET_TYPE:
return query.TargetColumnTargetType
case action.TargetFieldName_TARGET_FIELD_NAME_URL:
return query.TargetColumnURL
case action.TargetFieldName_TARGET_FIELD_NAME_TIMEOUT:
return query.TargetColumnTimeout
case action.TargetFieldName_TARGET_FIELD_NAME_INTERRUPT_ON_ERROR:
return query.TargetColumnInterruptOnError
default:
return query.TargetColumnCreationDate
}
}
// executionFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination
func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.Column {
if field == nil {
return query.ExecutionColumnCreationDate
}
switch *field {
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED:
return query.ExecutionColumnID
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID:
return query.ExecutionColumnID
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE:
return query.ExecutionColumnCreationDate
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CHANGED_DATE:
return query.ExecutionColumnChangeDate
default:
return query.ExecutionColumnCreationDate
}
}
func (s *Server) searchExecutionsRequestToModel(req *action.SearchExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc, err := resource_object.SearchQueryPbToQuery(s.systemDefaults, req.Query)
if err != nil {
return nil, err
}
queries, err := executionQueriesToQuery(req.Filters)
if err != nil {
return nil, err
}
return &query.ExecutionSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: executionFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func executionQueriesToQuery(queries []*action.ExecutionSearchFilter) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = executionQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.SearchQuery, error) {
switch q := searchQuery.Filter.(type) {
case *action.ExecutionSearchFilter_InConditionsFilter:
return inConditionsQueryToQuery(q.InConditionsFilter)
case *action.ExecutionSearchFilter_ExecutionTypeFilter:
return executionTypeToQuery(q.ExecutionTypeFilter)
case *action.ExecutionSearchFilter_IncludeFilter:
include, err := conditionToInclude(q.IncludeFilter.GetInclude())
if err != nil {
return nil, err
}
return query.NewIncludeSearchQuery(include)
case *action.ExecutionSearchFilter_TargetFilter:
return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionTypeToQuery(q *action.ExecutionTypeFilter) (query.SearchQuery, error) {
switch q.ExecutionType {
case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
case action.ExecutionType_EXECUTION_TYPE_REQUEST:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest)
case action.ExecutionType_EXECUTION_TYPE_RESPONSE:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse)
case action.ExecutionType_EXECUTION_TYPE_EVENT:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent)
case action.ExecutionType_EXECUTION_TYPE_FUNCTION:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction)
default:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
}
}
func inConditionsQueryToQuery(q *action.InConditionsFilter) (query.SearchQuery, error) {
values := make([]string, len(q.GetConditions()))
for i, condition := range q.GetConditions() {
id, err := conditionToID(condition)
if err != nil {
return nil, err
}
values[i] = id
}
return query.NewExecutionInIDsSearchQuery(values)
}
func conditionToID(q *action.Condition) (string, error) {
switch t := q.GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
return cond.ID(domain.ExecutionTypeResponse), nil
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
return cond.ID(), nil
case *action.Condition_Function:
return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionsToPb(executions []*query.Execution) []*action.GetExecution {
e := make([]*action.GetExecution, len(executions))
for i, execution := range executions {
e[i] = executionToPb(execution)
}
return e
}
func executionToPb(e *query.Execution) *action.GetExecution {
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.GetExecution{
Details: resource_object.DomainToDetailsPb(&e.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, e.ResourceOwner),
Execution: &action.Execution{
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 conditionIDRequestResponseMethodSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}}
case conditionIDRequestResponseServiceSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case conditionIDAllSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}
default:
return nil
}
}
func includeResponseToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case conditionIDRequestResponseMethodSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}}
case conditionIDRequestResponseServiceSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case conditionIDAllSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}
default:
return nil
}
}
func includeEventToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case conditionIDEventGroupSegmentCount:
if strings.HasSuffix(id, command.EventGroupSuffix) {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}}
} else {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}}
}
case conditionIDAllSegmentCount:
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}
default:
return nil
}
}
func includeFunctionToCondition(id string) *action.Condition {
return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}}
}

View File

@@ -0,0 +1,898 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_GetTarget(t *testing.T) {
_, _, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
type args struct {
ctx context.Context
dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error
req *action.GetTargetRequest
}
tests := []struct {
name string
args args
want *action.GetTargetResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.GetTargetRequest{},
},
wantErr: true,
},
{
name: "not found",
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.GetTargetRequest{Id: "notexisting"},
},
wantErr: true,
},
{
name: "get, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
{
name: "get, async, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false)
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
{
name: "get, webhook interruptOnError, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true)
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
{
name: "get, call, ok",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false)
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Config: &action.Target{
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: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true)
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
return nil
},
req: &action.GetTargetRequest{},
},
want: &action.GetTargetResponse{
Target: &action.GetTarget{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
got, getErr := Tester.Client.ActionV3.GetTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, getErr, "Error: "+getErr.Error())
} else {
assert.NoError(t, getErr)
wantTarget := tt.want.GetTarget()
gotTarget := got.GetTarget()
integration.AssertResourceDetails(t, wantTarget.GetDetails(), gotTarget.GetDetails())
assert.Equal(t, wantTarget.GetConfig(), gotTarget.GetConfig())
}
})
}
}
func TestServer_ListTargets(t *testing.T) {
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
type args struct {
ctx context.Context
dep func(context.Context, *action.SearchTargetsRequest, *action.SearchTargetsResponse) error
req *action.SearchTargetsRequest
}
tests := []struct {
name string
args args
want *action.SearchTargetsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.SearchTargetsRequest{},
},
wantErr: true,
},
{
name: "list, not found",
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.SearchTargetsRequest{
Filters: []*action.TargetSearchFilter{
{Filter: &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{"notfound"},
},
},
},
},
},
},
want: &action.SearchTargetsResponse{
Details: &resource_object.ListDetails{
TotalResult: 0,
AppliedLimit: 100,
},
Result: []*action.GetTarget{},
},
},
{
name: "list single id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{resp.GetDetails().GetId()},
},
}
response.Details.Timestamp = resp.GetDetails().GetChanged()
response.Result[0].Details = resp.GetDetails()
response.Result[0].Config.Name = name
return nil
},
req: &action.SearchTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.SearchTargetsResponse{
Details: &resource_object.ListDetails{
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.GetTarget{
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
}, {
name: "list single name",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{
TargetNameFilter: &action.TargetNameFilter{
TargetName: name,
},
}
response.Details.Timestamp = resp.GetDetails().GetChanged()
response.Result[0].Details = resp.GetDetails()
response.Result[0].Config.Name = name
return nil
},
req: &action.SearchTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.SearchTargetsResponse{
Details: &resource_object.ListDetails{
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.GetTarget{
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instanceID,
},
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
},
{
name: "list multiple id",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchTargetsRequest, response *action.SearchTargetsResponse) error {
name1 := fmt.Sprint(time.Now().UnixNano() + 1)
name2 := fmt.Sprint(time.Now().UnixNano() + 3)
name3 := fmt.Sprint(time.Now().UnixNano() + 5)
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.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{
InTargetIdsFilter: &action.InTargetIDsFilter{
TargetIds: []string{resp1.GetDetails().GetId(), resp2.GetDetails().GetId(), resp3.GetDetails().GetId()},
},
}
response.Details.Timestamp = resp3.GetDetails().GetChanged()
response.Result[0].Details = resp1.GetDetails()
response.Result[0].Config.Name = name1
response.Result[1].Details = resp2.GetDetails()
response.Result[1].Config.Name = name2
response.Result[2].Details = resp3.GetDetails()
response.Result[2].Config.Name = name3
return nil
},
req: &action.SearchTargetsRequest{
Filters: []*action.TargetSearchFilter{{}},
},
},
want: &action.SearchTargetsResponse{
Details: &resource_object.ListDetails{
TotalResult: 3,
AppliedLimit: 100,
},
Result: []*action.GetTarget{
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instanceID,
},
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instanceID,
},
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instanceID,
},
},
Config: &action.Target{
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Tester.Client.ActionV3.SearchTargets(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(ttt, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
integration.AssertResourceDetails(t, tt.want.Result[i].GetDetails(), got.Result[i].GetDetails())
assert.Equal(ttt, tt.want.Result[i].GetConfig(), got.Result[i].GetConfig())
}
integration.AssertResourceListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func TestServer_SearchExecutions(t *testing.T) {
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
targetResp := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
ctx context.Context
dep func(context.Context, *action.SearchExecutionsRequest, *action.SearchExecutionsResponse) error
req *action.SearchExecutionsRequest
}
tests := []struct {
name string
args args
want *action.SearchExecutionsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.SearchExecutionsRequest{},
},
wantErr: true,
},
{
name: "list request single condition",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error {
cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
resp := Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
response.Details.Timestamp = resp.GetDetails().GetChanged()
// Set expected response with used values for SetExecution
response.Result[0].Details = resp.GetDetails()
response.Result[0].Condition = cond
return nil
},
req: &action.SearchExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
}},
},
},
}},
},
},
want: &action.SearchExecutionsResponse{
Details: &resource_object.ListDetails{
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.GetExecution{
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
},
},
},
{
name: "list request single target",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error {
target := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
// add target as Filter to the request
request.Filters[0] = &action.ExecutionSearchFilter{
Filter: &action.ExecutionSearchFilter_TargetFilter{
TargetFilter: &action.TargetFilter{
TargetId: target.GetDetails().GetId(),
},
},
}
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/UpdateAction",
},
},
},
}
targets := executionTargetsSingleTarget(target.GetDetails().GetId())
resp := Tester.SetExecution(ctx, t, cond, targets)
response.Details.Timestamp = resp.GetDetails().GetChanged()
response.Result[0].Details = resp.GetDetails()
response.Result[0].Condition = cond
response.Result[0].Execution.Targets = targets
return nil
},
req: &action.SearchExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{}},
},
},
want: &action.SearchExecutionsResponse{
Details: &resource_object.ListDetails{
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.GetExecution{
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
Condition: &action.Condition{},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(""),
},
},
},
},
}, {
name: "list request single include",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error {
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/GetAction",
},
},
},
}
Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetDetails().GetId()))
request.Filters[0].GetIncludeFilter().Include = cond
includeCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/ListActions",
},
},
},
}
includeTargets := executionTargetsSingleInclude(cond)
resp2 := Tester.SetExecution(ctx, t, includeCond, includeTargets)
response.Details.Timestamp = resp2.GetDetails().GetChanged()
response.Result[0].Details = resp2.GetDetails()
response.Result[0].Condition = includeCond
response.Result[0].Execution = &action.Execution{
Targets: includeTargets,
}
return nil
},
req: &action.SearchExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_IncludeFilter{
IncludeFilter: &action.IncludeFilter{},
},
}},
},
},
want: &action.SearchExecutionsResponse{
Details: &resource_object.ListDetails{
TotalResult: 1,
AppliedLimit: 100,
},
Result: []*action.GetExecution{
{
Details: &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
},
},
},
},
},
{
name: "list multiple conditions",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error {
cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0]
targets1 := executionTargetsSingleTarget(targetResp.GetDetails().GetId())
resp1 := Tester.SetExecution(ctx, t, cond1, targets1)
response.Result[0].Details = resp1.GetDetails()
response.Result[0].Condition = cond1
response.Result[0].Execution = &action.Execution{
Targets: targets1,
}
cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1]
targets2 := executionTargetsSingleTarget(targetResp.GetDetails().GetId())
resp2 := Tester.SetExecution(ctx, t, cond2, targets2)
response.Result[1].Details = resp2.GetDetails()
response.Result[1].Condition = cond2
response.Result[1].Execution = &action.Execution{
Targets: targets2,
}
cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2]
targets3 := executionTargetsSingleTarget(targetResp.GetDetails().GetId())
resp3 := Tester.SetExecution(ctx, t, cond3, targets3)
response.Result[2].Details = resp3.GetDetails()
response.Result[2].Condition = cond3
response.Result[2].Execution = &action.Execution{
Targets: targets3,
}
response.Details.Timestamp = resp3.GetDetails().GetChanged()
return nil
},
req: &action.SearchExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/GetSession",
},
},
},
},
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/CreateSession",
},
},
},
},
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2.SessionService/SetSession",
},
},
},
},
},
},
},
}},
},
},
want: &action.SearchExecutionsResponse{
Details: &resource_object.ListDetails{
TotalResult: 3,
AppliedLimit: 100,
},
Result: []*action.GetExecution{
{
Details: &resource_object.Details{
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
}, {
Details: &resource_object.Details{
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
}, {
Details: &resource_object.Details{
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
},
},
},
},
{
name: "list multiple conditions all types",
args: args{
ctx: isolatedIAMOwnerCTX,
dep: func(ctx context.Context, request *action.SearchExecutionsRequest, response *action.SearchExecutionsResponse) error {
targets := executionTargetsSingleTarget(targetResp.GetDetails().GetId())
for i, cond := range request.Filters[0].GetInConditionsFilter().GetConditions() {
resp := Tester.SetExecution(ctx, t, cond, targets)
response.Result[i].Details = resp.GetDetails()
response.Result[i].Condition = cond
response.Result[i].Execution = &action.Execution{
Targets: targets,
}
// filled with info of last sequence
response.Details.Timestamp = resp.GetDetails().GetChanged()
}
return nil
},
req: &action.SearchExecutionsRequest{
Filters: []*action.ExecutionSearchFilter{{
Filter: &action.ExecutionSearchFilter_InConditionsFilter{
InConditionsFilter: &action.InConditionsFilter{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}},
},
},
},
}},
},
},
want: &action.SearchExecutionsResponse{
Details: &resource_object.ListDetails{
TotalResult: 10,
AppliedLimit: 100,
},
Result: []*action.GetExecution{
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
{Details: &resource_object.Details{Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID}}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := isolatedIAMOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Tester.Client.ActionV3.SearchExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(t, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(t, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
// 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.AssertResourceListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func containExecution(executionList []*action.GetExecution, execution *action.GetExecution) (int, bool) {
for i, exec := range executionList {
if reflect.DeepEqual(exec.Details, execution.Details) {
return i, true
}
}
return 0, false
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
@@ -17,6 +18,7 @@ var _ action.ZITADELActionsServer = (*Server)(nil)
type Server struct {
action.UnimplementedZITADELActionsServer
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
@@ -27,6 +29,7 @@ type Server struct {
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
listActionFunctions func() []string,
@@ -34,6 +37,7 @@ func CreateServer(
listGRPCServices func() []string,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
ListActionFunctions: listActionFunctions,
@@ -62,7 +66,7 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterZITADELActionsHandler
}
func checkExecutionEnabled(ctx context.Context) error {
func checkActionsEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().Actions {
return nil
}

View File

@@ -13,49 +13,48 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
)
var (
CTX context.Context
Tester *integration.Tester
Client action.ZITADELActionsClient
IAMOwnerCTX, SystemCTX context.Context
Tester *integration.Tester
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
ctx, _, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.ActionV3
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
IAMOwnerCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
return m.Run()
}())
}
func ensureFeatureEnabled(t *testing.T) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{
func ensureFeatureEnabled(t *testing.T, iamOwnerCTX context.Context) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.Actions.GetEnabled() {
return
}
_, err = Tester.Client.FeatureV2.SetInstanceFeatures(CTX, &feature.SetInstanceFeaturesRequest{
_, err = Tester.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{
Actions: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
if ctxDeadline, ok := iamOwnerCTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(iamOwnerCTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)

View File

@@ -15,45 +15,45 @@ import (
)
func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
add := createTargetToCommand(req)
instance := targetOwnerInstance(ctx)
details, err := s.command.AddTarget(ctx, add, instance.Id)
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.AddTarget(ctx, add, instanceID)
if err != nil {
return nil, err
}
return &action.CreateTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, add.AggregateID),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
instance := targetOwnerInstance(ctx)
details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instance.Id)
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instanceID)
if err != nil {
return nil, err
}
return &action.PatchTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
instance := targetOwnerInstance(ctx)
details, err := s.command.DeleteTarget(ctx, req.GetId(), instance.Id)
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID)
if err != nil {
return nil, err
}
return &action.DeleteTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
@@ -112,10 +112,3 @@ func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget
}
return target
}
func targetOwnerInstance(ctx context.Context) *object.Owner {
return &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: authz.GetInstance(ctx).InstanceID(),
}
}

View File

@@ -5,6 +5,7 @@ package action_test
import (
"context"
"fmt"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
"testing"
"time"
@@ -15,13 +16,13 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_CreateTarget(t *testing.T) {
ensureFeatureEnabled(t)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
tests := []struct {
name string
ctx context.Context
@@ -39,7 +40,7 @@ func TestServer_CreateTarget(t *testing.T) {
},
{
name: "empty name",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: "",
},
@@ -47,7 +48,7 @@ func TestServer_CreateTarget(t *testing.T) {
},
{
name: "empty type",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: nil,
@@ -56,7 +57,7 @@ func TestServer_CreateTarget(t *testing.T) {
},
{
name: "empty webhook url",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.Target_RestWebhook{
@@ -67,7 +68,7 @@ func TestServer_CreateTarget(t *testing.T) {
},
{
name: "empty request response url",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.Target_RestCall{
@@ -78,7 +79,7 @@ func TestServer_CreateTarget(t *testing.T) {
},
{
name: "empty timeout",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
@@ -91,7 +92,7 @@ func TestServer_CreateTarget(t *testing.T) {
},
{
name: "async, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
@@ -101,16 +102,16 @@ func TestServer_CreateTarget(t *testing.T) {
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "webhook, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
@@ -122,16 +123,16 @@ func TestServer_CreateTarget(t *testing.T) {
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "webhook, interrupt on error, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
@@ -143,16 +144,16 @@ func TestServer_CreateTarget(t *testing.T) {
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "call, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
@@ -164,17 +165,17 @@ func TestServer_CreateTarget(t *testing.T) {
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "call, interruptOnError, ok",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
@@ -186,17 +187,17 @@ func TestServer_CreateTarget(t *testing.T) {
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
got, err := Tester.Client.ActionV3.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
require.Error(t, err)
return
@@ -208,7 +209,8 @@ func TestServer_CreateTarget(t *testing.T) {
}
func TestServer_PatchTarget(t *testing.T) {
ensureFeatureEnabled(t)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
type args struct {
ctx context.Context
req *action.PatchTargetRequest
@@ -223,7 +225,7 @@ func TestServer_PatchTarget(t *testing.T) {
{
name: "missing permission",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
@@ -244,7 +246,7 @@ func TestServer_PatchTarget(t *testing.T) {
return nil
},
args: args{
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
@@ -256,12 +258,12 @@ func TestServer_PatchTarget(t *testing.T) {
{
name: "change name, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
@@ -269,22 +271,22 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "change type, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestCall{
@@ -296,22 +298,22 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "change url, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
@@ -319,22 +321,22 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Timeout: durationpb.New(20 * time.Second),
@@ -342,22 +344,22 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
{
name: "change type async, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId()
targetID := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestAsync{
@@ -367,10 +369,10 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
@@ -380,8 +382,8 @@ func TestServer_PatchTarget(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
// We want to have the same response no matter how often we call the function
Client.PatchTarget(tt.args.ctx, tt.args.req)
got, err := Client.PatchTarget(tt.args.ctx, tt.args.req)
Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req)
got, err := Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
@@ -393,8 +395,9 @@ func TestServer_PatchTarget(t *testing.T) {
}
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
_, instanceID, _, isolatedIAMOwnerCTX := Tester.UseIsolatedInstance(t, IAMOwnerCTX, SystemCTX)
ensureFeatureEnabled(t, isolatedIAMOwnerCTX)
target := Tester.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
@@ -412,7 +415,7 @@ func TestServer_DeleteTarget(t *testing.T) {
},
{
name: "empty id",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.DeleteTargetRequest{
Id: "",
},
@@ -420,22 +423,22 @@ func TestServer_DeleteTarget(t *testing.T) {
},
{
name: "delete target",
ctx: CTX,
ctx: isolatedIAMOwnerCTX,
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
Id: instanceID,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.DeleteTarget(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return

View File

@@ -1,21 +1,77 @@
package object
import (
"fmt"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner, id string) *resources_object.Details {
details := &resources_object.Details{
Id: id,
Sequence: objectDetail.Sequence,
Owner: owner,
func DomainToDetailsPb(objectDetail *domain.ObjectDetails, ownerType object.OwnerType, ownerId string) *resource_object.Details {
details := &resource_object.Details{
Id: objectDetail.ID,
Owner: &object.Owner{
Type: ownerType,
Id: ownerId,
},
}
if !objectDetail.EventDate.IsZero() {
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
details.Changed = timestamppb.New(objectDetail.EventDate)
}
if !objectDetail.CreationDate.IsZero() {
details.Created = timestamppb.New(objectDetail.CreationDate)
}
return details
}
func ToSearchDetailsPb(request query.SearchRequest, response query.SearchResponse) *resource_object.ListDetails {
details := &resource_object.ListDetails{
AppliedLimit: request.Limit,
TotalResult: response.Count,
Timestamp: timestamppb.New(response.EventCreatedAt),
}
return details
}
func TextMethodPbToQuery(method resource_object.TextFilterMethod) query.TextComparison {
switch method {
case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS:
return query.TextEquals
case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE:
return query.TextEqualsIgnoreCase
case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH:
return query.TextStartsWith
case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE:
return query.TextStartsWithIgnoreCase
case resource_object.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS:
return query.TextContains
default:
return -1
}
}
func SearchQueryPbToQuery(defaults systemdefaults.SystemDefaults, query *resource_object.SearchQuery) (offset, limit uint64, asc bool, err error) {
limit = defaults.DefaultQueryLimit
asc = true
if query == nil {
return 0, limit, asc, nil
}
offset = query.Offset
if query.Desc {
asc = false
}
if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit {
return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded")
}
if query.Limit > 0 {
limit = uint64(query.Limit)
}
return offset, limit, asc, nil
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
object_v3 "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
)
const (
@@ -34,33 +35,67 @@ func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string,
func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ interface{}, err error) {
interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
for _, service := range idFromRequestsServices {
if !strings.HasPrefix(service, "/") {
service = "/" + service
}
if strings.HasPrefix(info.FullMethod, service) {
withInstanceIDProperty, ok := req.(interface{ GetInstanceId() string })
withInstanceIDProperty, ok := req.(interface {
GetInstanceId() string
})
if !ok {
return handler(ctx, req)
}
ctx = authz.WithInstanceID(ctx, withInstanceIDProperty.GetInstanceId())
instance, err := verifier.InstanceByID(ctx)
if err != nil {
notFoundErr := new(zerrors.NotFoundError)
if errors.As(err, &notFoundErr) {
notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil)
}
return nil, status.Error(codes.NotFound, err.Error())
}
return handler(authz.WithInstance(ctx, instance), req)
return addInstanceByID(interceptorCtx, req, handler, verifier, translator, withInstanceIDProperty.GetInstanceId())
}
}
explicitInstanceRequest, ok := req.(interface {
GetInstance() *object_v3.Instance
})
if ok {
instance := explicitInstanceRequest.GetInstance()
if id := instance.GetId(); id != "" {
return addInstanceByID(interceptorCtx, req, handler, verifier, translator, id)
}
if domain := instance.GetDomain(); domain != "" {
return addInstanceByDomain(interceptorCtx, req, handler, verifier, translator, domain)
}
}
return addInstanceByRequestedHost(interceptorCtx, req, handler, verifier, translator, externalDomain)
}
func addInstanceByID(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, id string) (interface{}, error) {
instance, err := verifier.InstanceByID(ctx, id)
if err != nil {
notFoundErr := new(zerrors.ZitadelError)
if errors.As(err, &notFoundErr) {
notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil)
}
return nil, status.Error(codes.NotFound, fmt.Errorf("unable to set instance using id %s: %w", id, notFoundErr).Error())
}
return handler(authz.WithInstance(ctx, instance), req)
}
func addInstanceByDomain(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, domain string) (interface{}, error) {
instance, err := verifier.InstanceByHost(ctx, domain, "")
if err != nil {
notFoundErr := new(zerrors.NotFoundError)
if errors.As(err, &notFoundErr) {
notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil)
}
return nil, status.Error(codes.NotFound, fmt.Errorf("unable to set instance using domain %s: %w", domain, notFoundErr).Error())
}
return handler(authz.WithInstance(ctx, instance), req)
}
func addInstanceByRequestedHost(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, externalDomain string) (interface{}, error) {
requestContext := zitadel_http.DomainContext(ctx)
if requestContext.InstanceHost == "" {
logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance")
logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).Error("unable to set instance")
return nil, status.Error(codes.NotFound, "no instanceHost specified")
}
instance, err := verifier.InstanceByHost(interceptorCtx, requestContext.InstanceHost, requestContext.PublicHost)
instance, err := verifier.InstanceByHost(ctx, requestContext.InstanceHost, requestContext.PublicHost)
if err != nil {
origin := zitadel_http.DomainContext(ctx)
logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance")
@@ -72,6 +107,5 @@ func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInf
}
return nil, status.Error(codes.NotFound, fmt.Sprintf("unable to set instance using origin %s (ExternalDomain is %s)", origin, externalDomain))
}
span.End()
return handler(authz.WithInstance(ctx, instance), req)
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/feature"
object_v3 "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
)
func Test_setInstance(t *testing.T) {
@@ -69,6 +70,135 @@ func Test_setInstance(t *testing.T) {
err: false,
},
},
{
"explicit instance unset, hostname not found, error",
args{
ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host2"}),
req: &mockRequestWithExplicitInstance{},
verifier: &mockInstanceVerifier{instanceHost: "host"},
},
res{
want: nil,
err: true,
},
},
{
"explicit instance unset, invalid host, error",
args{
ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host2"}),
req: &mockRequestWithExplicitInstance{},
verifier: &mockInstanceVerifier{instanceHost: "host"},
},
res{
want: nil,
err: true,
},
},
{
"explicit instance unset, valid host",
args{
ctx: http_util.WithDomainContext(context.Background(), &http_util.DomainCtx{InstanceHost: "host"}),
req: &mockRequestWithExplicitInstance{},
verifier: &mockInstanceVerifier{instanceHost: "host"},
handler: func(ctx context.Context, req interface{}) (interface{}, error) {
return req, nil
},
},
res{
want: &mockRequestWithExplicitInstance{},
err: false,
},
},
{
name: "explicit instance set, id not found, error",
args: args{
ctx: context.Background(),
req: &mockRequestWithExplicitInstance{
instance: object_v3.Instance{
Property: &object_v3.Instance_Id{
Id: "not existing instance id",
},
},
},
verifier: &mockInstanceVerifier{id: "existing instance id"},
},
res: res{
want: nil,
err: true,
},
},
{
name: "explicit instance set, id found, ok",
args: args{
ctx: context.Background(),
req: &mockRequestWithExplicitInstance{
instance: object_v3.Instance{
Property: &object_v3.Instance_Id{
Id: "existing instance id",
},
},
},
verifier: &mockInstanceVerifier{id: "existing instance id"},
handler: func(ctx context.Context, req interface{}) (interface{}, error) {
return req, nil
},
},
res: res{
want: &mockRequestWithExplicitInstance{
instance: object_v3.Instance{
Property: &object_v3.Instance_Id{
Id: "existing instance id",
},
},
},
err: false,
},
},
{
name: "explicit instance set, domain not found, error",
args: args{
ctx: context.Background(),
req: &mockRequestWithExplicitInstance{
instance: object_v3.Instance{
Property: &object_v3.Instance_Domain{
Domain: "not existing instance domain",
},
},
},
verifier: &mockInstanceVerifier{instanceHost: "existing instance domain"},
},
res: res{
want: nil,
err: true,
},
},
{
name: "explicit instance set, domain found, ok",
args: args{
ctx: context.Background(),
req: &mockRequestWithExplicitInstance{
instance: object_v3.Instance{
Property: &object_v3.Instance_Domain{
Domain: "existing instance domain",
},
},
},
verifier: &mockInstanceVerifier{instanceHost: "existing instance domain"},
handler: func(ctx context.Context, req interface{}) (interface{}, error) {
return req, nil
},
},
res: res{
want: &mockRequestWithExplicitInstance{
instance: object_v3.Instance{
Property: &object_v3.Instance_Domain{
Domain: "existing instance domain",
},
},
},
err: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -86,7 +216,16 @@ func Test_setInstance(t *testing.T) {
type mockRequest struct{}
type mockRequestWithExplicitInstance struct {
instance object_v3.Instance
}
func (m *mockRequestWithExplicitInstance) GetInstance() *object_v3.Instance {
return &m.instance
}
type mockInstanceVerifier struct {
id string
instanceHost string
publicHost string
}
@@ -104,7 +243,12 @@ func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, p
return &mockInstance{}, nil
}
func (m *mockInstanceVerifier) InstanceByID(context.Context) (authz.Instance, error) { return nil, nil }
func (m *mockInstanceVerifier) InstanceByID(_ context.Context, id string) (authz.Instance, error) {
if id != m.id {
return nil, fmt.Errorf("not found")
}
return &mockInstance{}, nil
}
type mockInstance struct{}

View File

@@ -2,7 +2,6 @@ package server
import (
"crypto/tls"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

View File

@@ -3,7 +3,6 @@ package system
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance"
"github.com/zitadel/zitadel/internal/api/grpc/member"
"github.com/zitadel/zitadel/internal/api/grpc/object"
@@ -151,12 +150,6 @@ func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequ
}
func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) (*system_pb.AddDomainResponse, error) {
instance, err := s.query.InstanceByID(ctx)
if err != nil {
return nil, err
}
ctx = authz.WithInstance(ctx, instance)
details, err := s.command.AddInstanceDomain(ctx, req.Domain)
if err != nil {
return nil, err

View File

@@ -242,7 +242,7 @@ func (m *mockInstanceVerifier) InstanceByHost(_ context.Context, instanceHost, p
return &mockInstance{}, nil
}
func (m *mockInstanceVerifier) InstanceByID(context.Context) (authz.Instance, error) {
func (m *mockInstanceVerifier) InstanceByID(context.Context, string) (authz.Instance, error) {
return nil, nil
}