diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 52b6666435..5db2d941ac 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1011,6 +1011,7 @@ InternalAuthZ: - "events.read" - "milestones.read" - "session.delete" + - "execution.target.read" - "execution.target.write" - "execution.target.delete" - "execution.read" @@ -1048,6 +1049,8 @@ InternalAuthZ: - "project.grant.member.read" - "events.read" - "milestones.read" + - "execution.target.read" + - "execution.read" - Role: "IAM_ORG_MANAGER" Permissions: - "org.read" diff --git a/internal/api/grpc/execution/v3alpha/execution.go b/internal/api/grpc/execution/v3alpha/execution.go index d455670130..5aff2e1098 100644 --- a/internal/api/grpc/execution/v3alpha/execution.go +++ b/internal/api/grpc/execution/v3alpha/execution.go @@ -37,7 +37,7 @@ func (s *Server) SetExecution(ctx context.Context, req *execution.SetExecutionRe var err error var details *domain.ObjectDetails switch t := req.GetCondition().GetConditionType().(type) { - case *execution.SetConditions_Request: + case *execution.Condition_Request: cond := &command.ExecutionAPICondition{ Method: t.Request.GetMethod(), Service: t.Request.GetService(), @@ -47,7 +47,7 @@ func (s *Server) SetExecution(ctx context.Context, req *execution.SetExecutionRe if err != nil { return nil, err } - case *execution.SetConditions_Response: + case *execution.Condition_Response: cond := &command.ExecutionAPICondition{ Method: t.Response.GetMethod(), Service: t.Response.GetService(), @@ -57,7 +57,7 @@ func (s *Server) SetExecution(ctx context.Context, req *execution.SetExecutionRe if err != nil { return nil, err } - case *execution.SetConditions_Event: + case *execution.Condition_Event: cond := &command.ExecutionEventCondition{ Event: t.Event.GetEvent(), Group: t.Event.GetGroup(), @@ -67,7 +67,7 @@ func (s *Server) SetExecution(ctx context.Context, req *execution.SetExecutionRe if err != nil { return nil, err } - case *execution.SetConditions_Function: + case *execution.Condition_Function: details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), set, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err @@ -82,7 +82,7 @@ func (s *Server) DeleteExecution(ctx context.Context, req *execution.DeleteExecu var err error var details *domain.ObjectDetails switch t := req.GetCondition().GetConditionType().(type) { - case *execution.SetConditions_Request: + case *execution.Condition_Request: cond := &command.ExecutionAPICondition{ Method: t.Request.GetMethod(), Service: t.Request.GetService(), @@ -92,7 +92,7 @@ func (s *Server) DeleteExecution(ctx context.Context, req *execution.DeleteExecu if err != nil { return nil, err } - case *execution.SetConditions_Response: + case *execution.Condition_Response: cond := &command.ExecutionAPICondition{ Method: t.Response.GetMethod(), Service: t.Response.GetService(), @@ -102,7 +102,7 @@ func (s *Server) DeleteExecution(ctx context.Context, req *execution.DeleteExecu if err != nil { return nil, err } - case *execution.SetConditions_Event: + case *execution.Condition_Event: cond := &command.ExecutionEventCondition{ Event: t.Event.GetEvent(), Group: t.Event.GetGroup(), @@ -112,7 +112,7 @@ func (s *Server) DeleteExecution(ctx context.Context, req *execution.DeleteExecu if err != nil { return nil, err } - case *execution.SetConditions_Function: + case *execution.Condition_Function: details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err diff --git a/internal/api/grpc/execution/v3alpha/execution_integration_test.go b/internal/api/grpc/execution/v3alpha/execution_integration_test.go index 01b8b141d9..59f07d7303 100644 --- a/internal/api/grpc/execution/v3alpha/execution_integration_test.go +++ b/internal/api/grpc/execution/v3alpha/execution_integration_test.go @@ -28,10 +28,10 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_All{All: true}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_All{All: true}, }, }, }, @@ -42,9 +42,9 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{}, }, }, Targets: []string{targetResp.GetId()}, @@ -55,10 +55,10 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "method, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ Method: "/zitadel.session.v2beta.NotExistingService/List", }, }, @@ -72,10 +72,10 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "method, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/ListSessions", }, }, @@ -94,10 +94,10 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "service, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Service{ Service: "NotExistingService", }, }, @@ -111,10 +111,10 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "service, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Service{ Service: "zitadel.session.v2beta.SessionService", }, }, @@ -133,10 +133,10 @@ func TestServer_SetExecution_Request(t *testing.T) { name: "all, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_All{ All: true, }, }, @@ -170,16 +170,17 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { targetResp := Tester.CreateTarget(CTX, t) executionCond := "request" Tester.SetExecution(CTX, t, - &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_All{ + &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_All{ All: true, }, }, }, }, []string{targetResp.GetId()}, + []string{}, ) tests := []struct { @@ -193,10 +194,10 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { name: "method, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/ListSessions", }, }, @@ -215,10 +216,10 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { name: "service, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Service{ Service: "zitadel.session.v2beta.SessionService", }, }, @@ -237,10 +238,10 @@ func TestServer_SetExecution_Request_Include(t *testing.T) { name: "all, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_All{ All: true, }, }, @@ -285,10 +286,10 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_All{All: true}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_All{All: true}, }, }, }, @@ -299,9 +300,9 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{}, }, }, }, @@ -311,10 +312,10 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "method, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/NotExisting", }, }, @@ -327,14 +328,14 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "method, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/GetSession", }, }, @@ -352,10 +353,10 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "service, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Service{ Service: "NotExistingService", }, }, @@ -368,14 +369,14 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "service, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Service{ Service: "zitadel.user.v2beta.UserService", }, }, @@ -393,14 +394,14 @@ func TestServer_DeleteExecution_Request(t *testing.T) { name: "all, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Request{ - Request: &execution.SetRequestExecution{ - Condition: &execution.SetRequestExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_All{ All: true, }, }, @@ -448,10 +449,10 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_All{All: true}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_All{All: true}, }, }, }, @@ -462,9 +463,9 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{}, }, }, Targets: []string{targetResp.GetId()}, @@ -475,10 +476,10 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "method, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Method{ Method: "/zitadel.session.v2beta.NotExistingService/List", }, }, @@ -492,10 +493,10 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "method, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/ListSessions", }, }, @@ -514,10 +515,10 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "service, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Service{ Service: "NotExistingService", }, }, @@ -531,10 +532,10 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "service, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Service{ Service: "zitadel.session.v2beta.SessionService", }, }, @@ -553,10 +554,10 @@ func TestServer_SetExecution_Response(t *testing.T) { name: "all, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_All{ All: true, }, }, @@ -601,10 +602,10 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_All{ All: true, }, }, @@ -617,9 +618,9 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{}, }, }, }, @@ -629,10 +630,10 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "method, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/NotExisting", }, }, @@ -645,14 +646,14 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "method, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Method{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Method{ Method: "/zitadel.session.v2beta.SessionService/GetSession", }, }, @@ -670,10 +671,10 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "service, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Service{ Service: "NotExistingService", }, }, @@ -686,14 +687,14 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "service, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_Service{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_Service{ Service: "zitadel.user.v2beta.UserService", }, }, @@ -711,14 +712,14 @@ func TestServer_DeleteExecution_Response(t *testing.T) { name: "all, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_All{ All: true, }, }, @@ -766,10 +767,10 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_All{ All: true, }, }, @@ -782,9 +783,9 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{}, }, }, Targets: []string{targetResp.GetId()}, @@ -798,10 +799,10 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "event, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Event{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Event{ Event: "xxx", }, }, @@ -816,10 +817,10 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "event, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Event{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Event{ Event: "xxx", }, }, @@ -841,10 +842,10 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "group, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Group{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Group{ Group: "xxx", }, }, @@ -859,10 +860,10 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "group, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Group{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Group{ Group: "xxx", }, }, @@ -881,10 +882,10 @@ func TestServer_SetExecution_Event(t *testing.T) { name: "all, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_All{ All: true, }, }, @@ -929,10 +930,10 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_All{ All: true, }, }, @@ -945,9 +946,9 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{}, }, }, }, @@ -959,10 +960,10 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "event, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Event{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Event{ Event: "xxx", }, }, @@ -976,14 +977,14 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "event, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Event{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Event{ Event: "xxx", }, }, @@ -1001,10 +1002,10 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "group, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Group{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Group{ Group: "xxx", }, }, @@ -1017,14 +1018,14 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "group, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_Group{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_Group{ Group: "xxx", }, }, @@ -1042,10 +1043,10 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "all, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_All{ All: true, }, }, @@ -1063,14 +1064,14 @@ func TestServer_DeleteExecution_Event(t *testing.T) { name: "all, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Event{ - Event: &execution.SetEventExecution{ - Condition: &execution.SetEventExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Event{ + Event: &execution.EventExecution{ + Condition: &execution.EventExecution_All{ All: true, }, }, @@ -1118,10 +1119,10 @@ func TestServer_SetExecution_Function(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_All{All: true}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_All{All: true}, }, }, }, @@ -1132,9 +1133,9 @@ func TestServer_SetExecution_Function(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{}, }, }, Targets: []string{targetResp.GetId()}, @@ -1145,8 +1146,8 @@ func TestServer_SetExecution_Function(t *testing.T) { name: "function, not existing", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Function{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Function{ Function: "xxx", }, }, @@ -1158,8 +1159,8 @@ func TestServer_SetExecution_Function(t *testing.T) { name: "function, ok", ctx: CTX, req: &execution.SetExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Function{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Function{ Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication", }, }, @@ -1202,10 +1203,10 @@ func TestServer_DeleteExecution_Function(t *testing.T) { name: "missing permission", ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{ - Condition: &execution.SetResponseExecution_All{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{ + Condition: &execution.ResponseExecution_All{ All: true, }, }, @@ -1218,9 +1219,9 @@ func TestServer_DeleteExecution_Function(t *testing.T) { name: "no condition, error", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Response{ - Response: &execution.SetResponseExecution{}, + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Response{ + Response: &execution.ResponseExecution{}, }, }, }, @@ -1230,8 +1231,8 @@ func TestServer_DeleteExecution_Function(t *testing.T) { name: "function, not existing", ctx: CTX, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Function{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Function{ Function: "xxx", }, }, @@ -1242,12 +1243,12 @@ func TestServer_DeleteExecution_Function(t *testing.T) { name: "function, ok", ctx: CTX, dep: func(ctx context.Context, request *execution.DeleteExecutionRequest) error { - Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}) + Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{}) return nil }, req: &execution.DeleteExecutionRequest{ - Condition: &execution.SetConditions{ - ConditionType: &execution.SetConditions_Function{ + Condition: &execution.Condition{ + ConditionType: &execution.Condition_Function{ Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication", }, }, diff --git a/internal/api/grpc/execution/v3alpha/query.go b/internal/api/grpc/execution/v3alpha/query.go new file mode 100644 index 0000000000..7c47a0a3c5 --- /dev/null +++ b/internal/api/grpc/execution/v3alpha/query.go @@ -0,0 +1,286 @@ +package execution + +import ( + "context" + + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha" +) + +func (s *Server) ListTargets(ctx context.Context, req *execution.ListTargetsRequest) (*execution.ListTargetsResponse, error) { + queries, err := listTargetsRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchTargets(ctx, queries) + if err != nil { + return nil, err + } + return &execution.ListTargetsResponse{ + Result: targetsToPb(resp.Targets), + Details: object.ToListDetails(resp.SearchResponse), + }, nil +} + +func listTargetsRequestToModel(req *execution.ListTargetsRequest) (*query.TargetSearchQueries, error) { + offset, limit, asc := object.ListQueryToQuery(req.Query) + queries, err := targetQueriesToQuery(req.Queries) + 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 targetFieldNameToSortingColumn(field execution.TargetFieldName) query.Column { + switch field { + case execution.TargetFieldName_FIELD_NAME_UNSPECIFIED: + return query.TargetColumnID + case execution.TargetFieldName_FIELD_NAME_ID: + return query.TargetColumnID + case execution.TargetFieldName_FIELD_NAME_CREATION_DATE: + return query.TargetColumnCreationDate + case execution.TargetFieldName_FIELD_NAME_CHANGE_DATE: + return query.TargetColumnChangeDate + case execution.TargetFieldName_FIELD_NAME_NAME: + return query.TargetColumnName + case execution.TargetFieldName_FIELD_NAME_TARGET_TYPE: + return query.TargetColumnTargetType + case execution.TargetFieldName_FIELD_NAME_URL: + return query.TargetColumnURL + case execution.TargetFieldName_FIELD_NAME_TIMEOUT: + return query.TargetColumnTimeout + case execution.TargetFieldName_FIELD_NAME_ASYNC: + return query.TargetColumnAsync + case execution.TargetFieldName_FIELD_NAME_INTERRUPT_ON_ERROR: + return query.TargetColumnInterruptOnError + default: + return query.TargetColumnID + } +} + +func targetQueriesToQuery(queries []*execution.TargetSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = targetQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func targetQueryToQuery(query *execution.TargetSearchQuery) (query.SearchQuery, error) { + switch q := query.Query.(type) { + case *execution.TargetSearchQuery_TargetNameQuery: + return targetNameQueryToQuery(q.TargetNameQuery) + case *execution.TargetSearchQuery_InTargetIdsQuery: + return targetInTargetIdsQueryToQuery(q.InTargetIdsQuery) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func targetNameQueryToQuery(q *execution.TargetNameQuery) (query.SearchQuery, error) { + return query.NewTargetNameSearchQuery(object.TextMethodToQuery(q.Method), q.GetTargetName()) +} + +func targetInTargetIdsQueryToQuery(q *execution.InTargetIDsQuery) (query.SearchQuery, error) { + return query.NewTargetInIDsSearchQuery(q.GetTargetIds()) +} + +func (s *Server) GetTargetByID(ctx context.Context, req *execution.GetTargetByIDRequest) (_ *execution.GetTargetByIDResponse, err error) { + resp, err := s.query.GetTargetByID(ctx, req.GetTargetId()) + if err != nil { + return nil, err + } + return &execution.GetTargetByIDResponse{ + Target: targetToPb(resp), + }, nil +} + +func targetsToPb(targets []*query.Target) []*execution.Target { + t := make([]*execution.Target, len(targets)) + for i, target := range targets { + t[i] = targetToPb(target) + } + return t +} + +func targetToPb(t *query.Target) *execution.Target { + target := &execution.Target{ + Details: object.DomainToDetailsPb(&t.ObjectDetails), + TargetId: t.ID, + Name: t.Name, + Timeout: durationpb.New(t.Timeout), + } + if t.Async { + target.ExecutionType = &execution.Target_IsAsync{IsAsync: t.Async} + } + if t.InterruptOnError { + target.ExecutionType = &execution.Target_InterruptOnError{InterruptOnError: t.InterruptOnError} + } + + switch t.TargetType { + case domain.TargetTypeWebhook: + target.TargetType = &execution.Target_RestWebhook{RestWebhook: &execution.SetRESTWebhook{Url: t.URL}} + case domain.TargetTypeRequestResponse: + target.TargetType = &execution.Target_RestRequestResponse{RestRequestResponse: &execution.SetRESTRequestResponse{Url: t.URL}} + default: + target.TargetType = nil + } + return target +} + +func (s *Server) ListExecutions(ctx context.Context, req *execution.ListExecutionsRequest) (*execution.ListExecutionsResponse, error) { + queries, err := listExecutionsRequestToModel(req) + if err != nil { + return nil, err + } + resp, err := s.query.SearchExecutions(ctx, queries) + if err != nil { + return nil, err + } + return &execution.ListExecutionsResponse{ + Result: executionsToPb(resp.Executions), + Details: object.ToListDetails(resp.SearchResponse), + }, nil +} + +func listExecutionsRequestToModel(req *execution.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) { + offset, limit, asc := object.ListQueryToQuery(req.Query) + queries, err := executionQueriesToQuery(req.Queries) + if err != nil { + return nil, err + } + return &query.ExecutionSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} + +func executionQueriesToQuery(queries []*execution.SearchQuery) (_ []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 *execution.SearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.Query.(type) { + case *execution.SearchQuery_InConditionsQuery: + return inConditionsQueryToQuery(q.InConditionsQuery) + case *execution.SearchQuery_ExecutionTypeQuery: + return executionTypeToQuery(q.ExecutionTypeQuery) + case *execution.SearchQuery_TargetQuery: + return query.NewExecutionTargetSearchQuery(q.TargetQuery.GetTargetId()) + case *execution.SearchQuery_IncludeQuery: + return query.NewExecutionIncludeSearchQuery(q.IncludeQuery.GetInclude()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func executionTypeToQuery(q *execution.ExecutionTypeQuery) (query.SearchQuery, error) { + switch q.ExecutionType { + case execution.ExecutionType_EXECUTION_TYPE_UNSPECIFIED: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) + case execution.ExecutionType_EXECUTION_TYPE_REQUEST: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest) + case execution.ExecutionType_EXECUTION_TYPE_RESPONSE: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse) + case execution.ExecutionType_EXECUTION_TYPE_EVENT: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent) + case execution.ExecutionType_EXECUTION_TYPE_FUNCTION: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction) + default: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) + } +} + +func inConditionsQueryToQuery(q *execution.InConditionsQuery) (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 *execution.Condition) (string, error) { + switch t := q.GetConditionType().(type) { + case *execution.Condition_Request: + cond := &command.ExecutionAPICondition{ + Method: t.Request.GetMethod(), + Service: t.Request.GetService(), + All: t.Request.GetAll(), + } + return cond.ID(domain.ExecutionTypeRequest), nil + case *execution.Condition_Response: + cond := &command.ExecutionAPICondition{ + Method: t.Response.GetMethod(), + Service: t.Response.GetService(), + All: t.Response.GetAll(), + } + return cond.ID(domain.ExecutionTypeResponse), nil + case *execution.Condition_Event: + cond := &command.ExecutionEventCondition{ + Event: t.Event.GetEvent(), + Group: t.Event.GetGroup(), + All: t.Event.GetAll(), + } + return cond.ID(), nil + case *execution.Condition_Function: + return t.Function, nil + default: + return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func executionsToPb(executions []*query.Execution) []*execution.Execution { + e := make([]*execution.Execution, len(executions)) + for i, execution := range executions { + e[i] = executionToPb(execution) + } + return e +} + +func executionToPb(e *query.Execution) *execution.Execution { + var targets, includes []string + if len(e.Targets) > 0 { + targets = e.Targets + } + if len(e.Includes) > 0 { + includes = e.Includes + } + return &execution.Execution{ + Details: object.DomainToDetailsPb(&e.ObjectDetails), + ExecutionId: e.ID, + Targets: targets, + Includes: includes, + } +} diff --git a/internal/api/grpc/execution/v3alpha/query_integration_test.go b/internal/api/grpc/execution/v3alpha/query_integration_test.go new file mode 100644 index 0000000000..d923a50161 --- /dev/null +++ b/internal/api/grpc/execution/v3alpha/query_integration_test.go @@ -0,0 +1,715 @@ +//go:build integration + +package execution_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/integration" + execution "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" +) + +func TestServer_GetTargetByID(t *testing.T) { + type args struct { + ctx context.Context + dep func(context.Context, *execution.GetTargetByIDRequest, *execution.GetTargetByIDResponse) error + req *execution.GetTargetByIDRequest + } + tests := []struct { + name string + args args + want *execution.GetTargetByIDResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.GetTargetByIDRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: CTX, + req: &execution.GetTargetByIDRequest{TargetId: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.GetTargetByIDRequest, response *execution.GetTargetByIDResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false) + request.TargetId = resp.GetId() + + response.Target.TargetId = resp.GetId() + response.Target.Name = name + response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() + response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Target.Details.Sequence = resp.GetDetails().GetSequence() + return nil + }, + req: &execution.GetTargetByIDRequest{}, + }, + want: &execution.GetTargetByIDResponse{ + Target: &execution.Target{ + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + { + name: "get, async, ok", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.GetTargetByIDRequest, response *execution.GetTargetByIDResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTargetWithNameAndType(ctx, t, name, true, false) + request.TargetId = resp.GetId() + + response.Target.TargetId = resp.GetId() + response.Target.Name = name + response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() + response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Target.Details.Sequence = resp.GetDetails().GetSequence() + return nil + }, + req: &execution.GetTargetByIDRequest{}, + }, + want: &execution.GetTargetByIDResponse{ + Target: &execution.Target{ + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + ExecutionType: &execution.Target_IsAsync{IsAsync: true}, + }, + }, + }, + { + name: "get, interruptOnError, ok", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.GetTargetByIDRequest, response *execution.GetTargetByIDResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, true) + request.TargetId = resp.GetId() + + response.Target.TargetId = resp.GetId() + response.Target.Name = name + response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner() + response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Target.Details.Sequence = resp.GetDetails().GetSequence() + return nil + }, + req: &execution.GetTargetByIDRequest{}, + }, + want: &execution.GetTargetByIDResponse{ + Target: &execution.Target{ + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + ExecutionType: &execution.Target_InterruptOnError{InterruptOnError: true}, + }, + }, + }, + } + 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 := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, getErr := Client.GetTargetByID(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, getErr, "Error: "+getErr.Error()) + } else { + assert.NoError(ttt, getErr) + } + if getErr != nil { + fmt.Println("Error: " + getErr.Error()) + return + } + + integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget()) + + assert.Equal(t, tt.want.Target, got.Target) + + }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListTargets(t *testing.T) { + type args struct { + ctx context.Context + dep func(context.Context, *execution.ListTargetsRequest, *execution.ListTargetsResponse) error + req *execution.ListTargetsRequest + } + tests := []struct { + name string + args args + want *execution.ListTargetsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.ListTargetsRequest{}, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: CTX, + req: &execution.ListTargetsRequest{ + Queries: []*execution.TargetSearchQuery{ + {Query: &execution.TargetSearchQuery_InTargetIdsQuery{ + InTargetIdsQuery: &execution.InTargetIDsQuery{ + TargetIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &execution.ListTargetsResponse{ + Details: &object.ListDetails{ + TotalResult: 0, + }, + Result: []*execution.Target{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListTargetsRequest, response *execution.ListTargetsResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false) + request.Queries[0].Query = &execution.TargetSearchQuery_InTargetIdsQuery{ + InTargetIdsQuery: &execution.InTargetIDsQuery{ + TargetIds: []string{resp.GetId()}, + }, + } + response.Details.Timestamp = resp.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + + response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() + response.Result[0].TargetId = resp.GetId() + response.Result[0].Name = name + return nil + }, + req: &execution.ListTargetsRequest{ + Queries: []*execution.TargetSearchQuery{{}}, + }, + }, + want: &execution.ListTargetsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*execution.Target{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, { + name: "list single name", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListTargetsRequest, response *execution.ListTargetsResponse) error { + name := fmt.Sprint(time.Now().UnixNano() + 1) + resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false) + request.Queries[0].Query = &execution.TargetSearchQuery_TargetNameQuery{ + TargetNameQuery: &execution.TargetNameQuery{ + TargetName: name, + }, + } + response.Details.Timestamp = resp.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + + response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() + response.Result[0].TargetId = resp.GetId() + response.Result[0].Name = name + return nil + }, + req: &execution.ListTargetsRequest{ + Queries: []*execution.TargetSearchQuery{{}}, + }, + }, + want: &execution.ListTargetsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*execution.Target{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListTargetsRequest, response *execution.ListTargetsResponse) error { + name1 := fmt.Sprint(time.Now().UnixNano() + 1) + name2 := fmt.Sprint(time.Now().UnixNano() + 3) + name3 := fmt.Sprint(time.Now().UnixNano() + 5) + resp1 := Tester.CreateTargetWithNameAndType(ctx, t, name1, false, false) + resp2 := Tester.CreateTargetWithNameAndType(ctx, t, name2, true, false) + resp3 := Tester.CreateTargetWithNameAndType(ctx, t, name3, false, true) + request.Queries[0].Query = &execution.TargetSearchQuery_InTargetIdsQuery{ + InTargetIdsQuery: &execution.InTargetIDsQuery{ + TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + response.Details.Timestamp = resp3.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp3.GetDetails().GetSequence() + + response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence() + response.Result[0].TargetId = resp1.GetId() + response.Result[0].Name = name1 + response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate() + response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence() + response.Result[1].TargetId = resp2.GetId() + response.Result[1].Name = name2 + response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate() + response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence() + response.Result[2].TargetId = resp3.GetId() + response.Result[2].Name = name3 + return nil + }, + req: &execution.ListTargetsRequest{ + Queries: []*execution.TargetSearchQuery{{}}, + }, + }, + want: &execution.ListTargetsResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + }, + Result: []*execution.Target{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + ExecutionType: &execution.Target_IsAsync{IsAsync: true}, + }, + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + TargetType: &execution.Target_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + ExecutionType: &execution.Target_InterruptOnError{InterruptOnError: true}, + }, + }, + }, + }, + } + 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 := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Client.ListTargets(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 { + assert.Contains(ttt, got.Result, tt.want.Result[i]) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListExecutions_Request(t *testing.T) { + targetResp := Tester.CreateTarget(CTX, t) + + type args struct { + ctx context.Context + dep func(context.Context, *execution.ListExecutionsRequest, *execution.ListExecutionsResponse) error + req *execution.ListExecutionsRequest + } + tests := []struct { + name string + args args + want *execution.ListExecutionsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner), + req: &execution.ListExecutionsRequest{}, + }, + wantErr: true, + }, + { + name: "list single condition", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListExecutionsRequest, response *execution.ListExecutionsResponse) error { + resp := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{}) + + response.Details.Timestamp = resp.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + + response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() + return nil + }, + req: &execution.ListExecutionsRequest{ + Queries: []*execution.SearchQuery{{ + Query: &execution.SearchQuery_InConditionsQuery{ + InConditionsQuery: &execution.InConditionsQuery{ + Conditions: []*execution.Condition{{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/GetSession", + }, + }, + }, + }, + }, + }, + }, + }}, + }, + }, + want: &execution.ListExecutionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*execution.Execution{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession", + Targets: []string{targetResp.GetId()}, + }, + }, + }, + }, + { + name: "list single target", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListExecutionsRequest, response *execution.ListExecutionsResponse) error { + target := Tester.CreateTarget(ctx, t) + // add target as query to the request + request.Queries[0] = &execution.SearchQuery{ + Query: &execution.SearchQuery_TargetQuery{ + TargetQuery: &execution.TargetQuery{ + TargetId: target.GetId(), + }, + }, + } + resp := Tester.SetExecution(ctx, t, &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/UpdateAction", + }, + }, + }, + }, []string{target.GetId()}, []string{}) + + response.Details.Timestamp = resp.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp.GetDetails().GetSequence() + + response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp.GetDetails().GetSequence() + response.Result[0].Targets[0] = target.GetId() + return nil + }, + req: &execution.ListExecutionsRequest{ + Queries: []*execution.SearchQuery{{}}, + }, + }, + want: &execution.ListExecutionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*execution.Execution{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + ExecutionId: "request./zitadel.management.v1.ManagementService/UpdateAction", + Targets: []string{""}, + }, + }, + }, + }, { + name: "list single include", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListExecutionsRequest, response *execution.ListExecutionsResponse) error { + Tester.SetExecution(ctx, t, &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/GetAction", + }, + }, + }, + }, []string{targetResp.GetId()}, []string{}) + resp2 := Tester.SetExecution(ctx, t, &execution.Condition{ + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/ListActions", + }, + }, + }, + }, []string{}, []string{"request./zitadel.management.v1.ManagementService/GetAction"}) + + response.Details.Timestamp = resp2.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp2.GetDetails().GetSequence() + + response.Result[0].Details.ChangeDate = resp2.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp2.GetDetails().GetSequence() + return nil + }, + req: &execution.ListExecutionsRequest{ + Queries: []*execution.SearchQuery{{ + Query: &execution.SearchQuery_IncludeQuery{ + IncludeQuery: &execution.IncludeQuery{Include: "request./zitadel.management.v1.ManagementService/GetAction"}, + }, + }}, + }, + }, + want: &execution.ListExecutionsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + }, + Result: []*execution.Execution{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + ExecutionId: "request./zitadel.management.v1.ManagementService/ListActions", + Includes: []string{"request./zitadel.management.v1.ManagementService/GetAction"}, + }, + }, + }, + }, + { + name: "list multiple conditions", + args: args{ + ctx: CTX, + dep: func(ctx context.Context, request *execution.ListExecutionsRequest, response *execution.ListExecutionsResponse) error { + + resp1 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{}) + response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate() + response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence() + + resp2 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[1], []string{targetResp.GetId()}, []string{}) + response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate() + response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence() + + resp3 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[2], []string{targetResp.GetId()}, []string{}) + response.Details.Timestamp = resp3.GetDetails().GetChangeDate() + response.Details.ProcessedSequence = resp3.GetDetails().GetSequence() + response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate() + response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence() + return nil + }, + req: &execution.ListExecutionsRequest{ + Queries: []*execution.SearchQuery{{ + Query: &execution.SearchQuery_InConditionsQuery{ + InConditionsQuery: &execution.InConditionsQuery{ + Conditions: []*execution.Condition{ + { + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/GetSession", + }, + }, + }, + }, + { + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/CreateSession", + }, + }, + }, + }, + { + ConditionType: &execution.Condition_Request{ + Request: &execution.RequestExecution{ + Condition: &execution.RequestExecution_Method{ + Method: "/zitadel.session.v2beta.SessionService/SetSession", + }, + }, + }, + }, + }, + }, + }, + }}, + }, + }, + want: &execution.ListExecutionsResponse{ + Details: &object.ListDetails{ + TotalResult: 3, + }, + Result: []*execution.Execution{ + { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession", + Targets: []string{targetResp.GetId()}, + }, { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + ExecutionId: "request./zitadel.session.v2beta.SessionService/CreateSession", + Targets: []string{targetResp.GetId()}, + }, { + Details: &object.Details{ + ResourceOwner: Tester.Instance.InstanceID(), + }, + ExecutionId: "request./zitadel.session.v2beta.SessionService/SetSession", + Targets: []string{targetResp.GetId()}, + }, + }, + }, + }, + } + 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 := CTX.Deadline(); ok { + retryDuration = time.Until(ctxDeadline) + } + + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Client.ListExecutions(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, listErr, "Error: "+listErr.Error()) + } 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 { + assert.Contains(ttt, got.Result, tt.want.Result[i]) + } + integration.AssertListDetails(t, tt.want, got) + }, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/execution/v3alpha/target_test.go b/internal/api/grpc/execution/v3alpha/target_test.go index 2d266f10ed..02939e5154 100644 --- a/internal/api/grpc/execution/v3alpha/target_test.go +++ b/internal/api/grpc/execution/v3alpha/target_test.go @@ -27,7 +27,6 @@ func Test_createTargetToCommand(t *testing.T) { args: args{nil}, want: &command.AddTarget{ Name: "", - TargetType: domain.TargetTypeUnspecified, URL: "", Timeout: 0, Async: false, diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index a9fafc9203..6a40eadf5e 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -2360,6 +2360,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { }) } } + func TestCommands_DeleteExecutionFunction(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/domain/target.go b/internal/domain/target.go index d4a8027395..83ab85478e 100644 --- a/internal/domain/target.go +++ b/internal/domain/target.go @@ -3,11 +3,8 @@ package domain type TargetType uint const ( - TargetTypeUnspecified TargetType = iota - TargetTypeWebhook + TargetTypeWebhook TargetType = iota TargetTypeRequestResponse - - TargetTypeStateCount ) type TargetState int32 diff --git a/internal/integration/assert.go b/internal/integration/assert.go index a12c812673..d5b7353aa2 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -63,9 +63,9 @@ func AssertListDetails[D ListDetailsMsg](t testing.TB, expected, actual D) { return } - gotCD := gotDetails.GetTimestamp().AsTime() - now := time.Now() - assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult()) + + gotCD := gotDetails.GetTimestamp().AsTime() + wantCD := time.Now() + assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute)) } diff --git a/internal/integration/client.go b/internal/integration/client.go index 7d97e2a188..6aa89d94e4 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -520,7 +520,7 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj } func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *execution.CreateTargetResponse { - target, err := s.Client.ExecutionV3.CreateTarget(ctx, &execution.CreateTargetRequest{ + req := &execution.CreateTargetRequest{ Name: fmt.Sprint(time.Now().UnixNano() + 1), TargetType: &execution.CreateTargetRequest_RestWebhook{ RestWebhook: &execution.SetRESTWebhook{ @@ -528,18 +528,42 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *execution.Crea }, }, Timeout: durationpb.New(10 * time.Second), - ExecutionType: &execution.CreateTargetRequest_InterruptOnError{ - InterruptOnError: true, - }, - }) + } + target, err := s.Client.ExecutionV3.CreateTarget(ctx, req) require.NoError(t, err) return target } -func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *execution.SetConditions, targets []string) *execution.SetExecutionResponse { +func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T, name string, async bool, interrupt bool) *execution.CreateTargetResponse { + req := &execution.CreateTargetRequest{ + Name: name, + TargetType: &execution.CreateTargetRequest_RestWebhook{ + RestWebhook: &execution.SetRESTWebhook{ + Url: "https://example.com", + }, + }, + Timeout: durationpb.New(10 * time.Second), + } + if async { + req.ExecutionType = &execution.CreateTargetRequest_IsAsync{ + IsAsync: true, + } + } + if interrupt { + req.ExecutionType = &execution.CreateTargetRequest_InterruptOnError{ + InterruptOnError: true, + } + } + target, err := s.Client.ExecutionV3.CreateTarget(ctx, req) + require.NoError(t, err) + return target +} + +func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *execution.Condition, targets []string, includes []string) *execution.SetExecutionResponse { target, err := s.Client.ExecutionV3.SetExecution(ctx, &execution.SetExecutionRequest{ Condition: cond, Targets: targets, + Includes: includes, }) require.NoError(t, err) return target diff --git a/internal/integration/integration.go b/internal/integration/integration.go index dff41e811e..ebca10d3ed 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -318,6 +318,7 @@ func (s *Tester) Done() { // // Note: the database must already be setup and initialized before // using NewTester. See the CONTRIBUTING.md document for details. + func NewTester(ctx context.Context, zitadelConfigYAML ...string) *Tester { args := strings.Split(commandLine, " ") diff --git a/internal/query/current_state.go b/internal/query/current_state.go index eff2f05398..b54d67853b 100644 --- a/internal/query/current_state.go +++ b/internal/query/current_state.go @@ -12,7 +12,6 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" @@ -20,6 +19,10 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +type Stateful interface { + SetState(*State) +} + type State struct { LastRun time.Time @@ -83,29 +86,7 @@ func (q *Queries) SearchCurrentStates(ctx context.Context, queries *CurrentState } func (q *Queries) latestState(ctx context.Context, projections ...table) (state *State, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - query, scan := prepareLatestState(ctx, q.client) - or := make(sq.Or, len(projections)) - for i, projection := range projections { - or[i] = sq.Eq{CurrentStateColProjectionName.identifier(): projection.name} - } - stmt, args, err := query. - Where(or). - Where(sq.Eq{CurrentStateColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}). - OrderBy(CurrentStateColEventDate.identifier() + " DESC"). - ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-5CfX9", "Errors.Query.SQLStatement") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - state, err = scan(row) - return err - }, stmt, args...) - - return state, err + return latestState(ctx, q.client, projections...) } func (q *Queries) ClearCurrentSequence(ctx context.Context, projectionName string) (err error) { diff --git a/internal/query/execution.go b/internal/query/execution.go new file mode 100644 index 0000000000..5a8a7e0f79 --- /dev/null +++ b/internal/query/execution.go @@ -0,0 +1,191 @@ +package query + +import ( + "context" + "database/sql" + "errors" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + executionTable = table{ + name: projection.ExecutionTable, + instanceIDCol: projection.ExecutionInstanceIDCol, + } + ExecutionColumnID = Column{ + name: projection.ExecutionIDCol, + table: executionTable, + } + ExecutionColumnCreationDate = Column{ + name: projection.ExecutionCreationDateCol, + table: executionTable, + } + ExecutionColumnChangeDate = Column{ + name: projection.ExecutionChangeDateCol, + table: executionTable, + } + ExecutionColumnResourceOwner = Column{ + name: projection.ExecutionResourceOwnerCol, + table: executionTable, + } + ExecutionColumnInstanceID = Column{ + name: projection.ExecutionInstanceIDCol, + table: executionTable, + } + ExecutionColumnSequence = Column{ + name: projection.ExecutionSequenceCol, + table: executionTable, + } + ExecutionColumnTargets = Column{ + name: projection.ExecutionTargetsCol, + table: executionTable, + } + ExecutionColumnIncludes = Column{ + name: projection.ExecutionIncludesCol, + table: executionTable, + } +) + +type Executions struct { + SearchResponse + Executions []*Execution +} + +func (e *Executions) SetState(s *State) { + e.State = s +} + +type Execution struct { + ID string + domain.ObjectDetails + + Targets database.TextArray[string] + Includes database.TextArray[string] +} + +type ExecutionSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *ExecutionSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func (q *Queries) SearchExecutions(ctx context.Context, queries *ExecutionSearchQueries) (executions *Executions, err error) { + eq := sq.Eq{ + ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + } + query, scan := prepareExecutionsQuery(ctx, q.client) + return genericRowsQueryWithState[*Executions](ctx, q.client, executionTable, combineToWhereStmt(query, queries.toQuery, eq), scan) +} + +func (q *Queries) GetExecutionByID(ctx context.Context, id string) (execution *Execution, err error) { + eq := sq.Eq{ + ExecutionColumnID.identifier(): id, + ExecutionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + } + query, scan := prepareExecutionQuery(ctx, q.client) + return genericRowQuery[*Execution](ctx, q.client, query.Where(eq), scan) +} + +func NewExecutionInIDsSearchQuery(values []string) (SearchQuery, error) { + return NewInTextQuery(ExecutionColumnID, values) +} + +func NewExecutionTypeSearchQuery(t domain.ExecutionType) (SearchQuery, error) { + return NewTextQuery(ExecutionColumnID, t.String(), TextStartsWith) +} + +func NewExecutionTargetSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(ExecutionColumnTargets, value, TextListContains) +} + +func NewExecutionIncludeSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(ExecutionColumnIncludes, value, TextListContains) +} + +func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { + return sq.Select( + ExecutionColumnID.identifier(), + ExecutionColumnChangeDate.identifier(), + ExecutionColumnResourceOwner.identifier(), + ExecutionColumnSequence.identifier(), + ExecutionColumnTargets.identifier(), + ExecutionColumnIncludes.identifier(), + countColumn.identifier(), + ).From(executionTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*Executions, error) { + executions := make([]*Execution, 0) + var count uint64 + for rows.Next() { + execution := new(Execution) + err := rows.Scan( + &execution.ID, + &execution.EventDate, + &execution.ResourceOwner, + &execution.Sequence, + &execution.Targets, + &execution.Includes, + &count, + ) + if err != nil { + return nil, err + } + executions = append(executions, execution) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-72xfx5jlj7", "Errors.Query.CloseRows") + } + + return &Executions{ + Executions: executions, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) { + return sq.Select( + ExecutionColumnID.identifier(), + ExecutionColumnChangeDate.identifier(), + ExecutionColumnResourceOwner.identifier(), + ExecutionColumnSequence.identifier(), + ExecutionColumnTargets.identifier(), + ExecutionColumnIncludes.identifier(), + ).From(executionTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Execution, error) { + execution := new(Execution) + err := row.Scan( + &execution.ID, + &execution.EventDate, + &execution.ResourceOwner, + &execution.Sequence, + &execution.Targets, + &execution.Includes, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal") + } + return execution, nil + } +} diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go new file mode 100644 index 0000000000..20058b1ae0 --- /dev/null +++ b/internal/query/execution_test.go @@ -0,0 +1,253 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + prepareExecutionsStmt = `SELECT projections.executions.id,` + + ` projections.executions.change_date,` + + ` projections.executions.resource_owner,` + + ` projections.executions.sequence,` + + ` projections.executions.targets,` + + ` projections.executions.includes,` + + ` COUNT(*) OVER ()` + + ` FROM projections.executions` + prepareExecutionsCols = []string{ + "id", + "change_date", + "resource_owner", + "sequence", + "targets", + "includes", + "count", + } + + prepareExecutionStmt = `SELECT projections.executions.id,` + + ` projections.executions.change_date,` + + ` projections.executions.resource_owner,` + + ` projections.executions.sequence,` + + ` projections.executions.targets,` + + ` projections.executions.includes` + + ` FROM projections.executions` + prepareExecutionCols = []string{ + "id", + "change_date", + "resource_owner", + "sequence", + "targets", + "includes", + } +) + +func Test_ExecutionPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareExecutionsQuery no result", + prepare: prepareExecutionsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareExecutionsStmt), + nil, + nil, + ), + }, + object: &Executions{Executions: []*Execution{}}, + }, + { + name: "prepareExecutionsQuery one result", + prepare: prepareExecutionsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareExecutionsStmt), + prepareExecutionsCols, + [][]driver.Value{ + { + "id", + testNow, + "ro", + uint64(20211109), + database.TextArray[string]{"target"}, + database.TextArray[string]{"include"}, + }, + }, + ), + }, + object: &Executions{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Executions: []*Execution{ + { + ID: "id", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + }, + Targets: database.TextArray[string]{"target"}, + Includes: database.TextArray[string]{"include"}, + }, + }, + }, + }, + { + name: "prepareExecutionsQuery multiple result", + prepare: prepareExecutionsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareExecutionsStmt), + prepareExecutionsCols, + [][]driver.Value{ + { + "id-1", + testNow, + "ro", + uint64(20211109), + database.TextArray[string]{"target1"}, + database.TextArray[string]{"include1"}, + }, + { + "id-2", + testNow, + "ro", + uint64(20211110), + database.TextArray[string]{"target2"}, + database.TextArray[string]{"include2"}, + }, + }, + ), + }, + object: &Executions{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Executions: []*Execution{ + { + ID: "id-1", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + }, + Targets: database.TextArray[string]{"target1"}, + Includes: database.TextArray[string]{"include1"}, + }, + { + ID: "id-2", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211110, + }, + Targets: database.TextArray[string]{"target2"}, + Includes: database.TextArray[string]{"include2"}, + }, + }, + }, + }, + { + name: "prepareExecutionsQuery sql err", + prepare: prepareExecutionsQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareExecutionsStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*Execution)(nil), + }, + { + name: "prepareExecutionQuery no result", + prepare: prepareExecutionQuery, + want: want{ + sqlExpectations: mockQueriesScanErr( + regexp.QuoteMeta(prepareExecutionStmt), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !zerrors.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Execution)(nil), + }, + { + name: "prepareExecutionQuery found", + prepare: prepareExecutionQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(prepareExecutionStmt), + prepareExecutionCols, + []driver.Value{ + "id", + testNow, + "ro", + uint64(20211109), + database.TextArray[string]{"target"}, + database.TextArray[string]{"include"}, + }, + ), + }, + object: &Execution{ + ID: "id", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + }, + Targets: database.TextArray[string]{"target"}, + Includes: database.TextArray[string]{"include"}, + }, + }, + { + name: "prepareExecutionQuery sql err", + prepare: prepareExecutionQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareExecutionStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*Execution)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + }) + } +} diff --git a/internal/query/generic.go b/internal/query/generic.go new file mode 100644 index 0000000000..70fc2884a2 --- /dev/null +++ b/internal/query/generic.go @@ -0,0 +1,109 @@ +package query + +import ( + "context" + "database/sql" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func genericRowsQuery[R any]( + ctx context.Context, + client *database.DB, + query sq.SelectBuilder, + scan func(rows *sql.Rows) (R, error), +) (resp R, err error) { + var rnil R + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + stmt, args, err := query.ToSql() + if err != nil { + return rnil, zerrors.ThrowInvalidArgument(err, "QUERY-05wf2q36ji", "Errors.Query.InvalidRequest") + } + err = client.QueryContext(ctx, func(rows *sql.Rows) error { + resp, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return rnil, zerrors.ThrowInternal(err, "QUERY-y2u7vctrha", "Errors.Internal") + } + return resp, err +} + +func genericRowsQueryWithState[R Stateful]( + ctx context.Context, + client *database.DB, + projection table, + query sq.SelectBuilder, + scan func(rows *sql.Rows) (R, error), +) (resp R, err error) { + var rnil R + resp, err = genericRowsQuery[R](ctx, client, query, scan) + if err != nil { + return rnil, err + } + state, err := latestState(ctx, client, projection) + if err != nil { + return rnil, err + } + resp.SetState(state) + return resp, err +} + +func latestState(ctx context.Context, client *database.DB, projections ...table) (state *State, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareLatestState(ctx, client) + or := make(sq.Or, len(projections)) + for i, projection := range projections { + or[i] = sq.Eq{CurrentStateColProjectionName.identifier(): projection.name} + } + stmt, args, err := query. + Where(or). + Where(sq.Eq{CurrentStateColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}). + OrderBy(CurrentStateColEventDate.identifier() + " DESC"). + ToSql() + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-5CfX9", "Errors.Query.SQLStatement") + } + + err = client.QueryRowContext(ctx, func(row *sql.Row) error { + state, err = scan(row) + return err + }, stmt, args...) + + return state, err +} + +func genericRowQuery[R any]( + ctx context.Context, + client *database.DB, + query sq.SelectBuilder, + scan func(row *sql.Row) (R, error), +) (resp R, err error) { + var rnil R + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + stmt, args, err := query.ToSql() + if err != nil { + return rnil, zerrors.ThrowInternal(err, "QUERY-s969t763z4", "Errors.Query.SQLStatement") + } + + err = client.QueryRowContext(ctx, func(row *sql.Row) error { + resp, err = scan(row) + return err + }, stmt, args...) + return resp, err +} + +func combineToWhereStmt(query sq.SelectBuilder, toQuery func(query sq.SelectBuilder) sq.SelectBuilder, eq interface{}) sq.SelectBuilder { + return toQuery(query).Where(eq) +} diff --git a/internal/query/projection/action_test.go b/internal/query/projection/action_test.go index 0f5e382737..cf9390ca4b 100644 --- a/internal/query/projection/action_test.go +++ b/internal/query/projection/action_test.go @@ -99,7 +99,7 @@ func TestActionProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - action.ChangedEventType, + action.DeactivatedEventType, action.AggregateType, []byte(`{}`), ), @@ -131,7 +131,7 @@ func TestActionProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - action.ChangedEventType, + action.ReactivatedEventType, action.AggregateType, []byte(`{}`), ), @@ -163,7 +163,7 @@ func TestActionProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - action.ChangedEventType, + action.RemovedEventType, action.AggregateType, []byte(`{}`), ), diff --git a/internal/query/projection/execution.go b/internal/query/projection/execution.go new file mode 100644 index 0000000000..350a27ec27 --- /dev/null +++ b/internal/query/projection/execution.go @@ -0,0 +1,109 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + exec "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +const ( + ExecutionTable = "projections.executions" + ExecutionIDCol = "id" + ExecutionCreationDateCol = "creation_date" + ExecutionChangeDateCol = "change_date" + ExecutionResourceOwnerCol = "resource_owner" + ExecutionInstanceIDCol = "instance_id" + ExecutionSequenceCol = "sequence" + ExecutionTargetsCol = "targets" + ExecutionIncludesCol = "includes" +) + +type executionProjection struct{} + +func newExecutionProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(executionProjection)) +} + +func (*executionProjection) Name() string { + return ExecutionTable +} + +func (*executionProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(ExecutionIDCol, handler.ColumnTypeText), + handler.NewColumn(ExecutionCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(ExecutionChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(ExecutionResourceOwnerCol, handler.ColumnTypeText), + handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(ExecutionSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(ExecutionTargetsCol, handler.ColumnTypeTextArray, handler.Nullable()), + handler.NewColumn(ExecutionIncludesCol, handler.ColumnTypeTextArray, handler.Nullable()), + }, + handler.NewPrimaryKey(ExecutionInstanceIDCol, ExecutionIDCol), + ), + ) +} + +func (p *executionProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: exec.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: exec.SetEventType, + Reduce: p.reduceExecutionSet, + }, + { + Event: exec.RemovedEventType, + Reduce: p.reduceExecutionRemoved, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(ExecutionInstanceIDCol), + }, + }, + }, + } +} + +func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*exec.SetEvent](event) + if err != nil { + return nil, err + } + columns := []handler.Column{ + handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(ExecutionIDCol, e.Aggregate().ID), + handler.NewCol(ExecutionResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(ExecutionCreationDateCol, handler.OnlySetValueOnInsert(ExecutionTable, e.CreationDate())), + handler.NewCol(ExecutionChangeDateCol, e.CreationDate()), + handler.NewCol(ExecutionSequenceCol, e.Sequence()), + handler.NewCol(ExecutionTargetsCol, e.Targets), + handler.NewCol(ExecutionIncludesCol, e.Includes), + } + return handler.NewUpsertStatement(e, columns[0:2], columns), nil +} + +func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*exec.RemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(ExecutionInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(ExecutionIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/execution_test.go b/internal/query/projection/execution_test.go new file mode 100644 index 0000000000..537e8e0586 --- /dev/null +++ b/internal/query/projection/execution_test.go @@ -0,0 +1,129 @@ +package projection + +import ( + "testing" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + exec "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestExecutionProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceExecutionSet", + args: args{ + event: getEvent( + testEvent( + exec.SetEventType, + exec.AggregateType, + []byte(`{"targets": ["target"], "includes": ["include"]}`), + ), + eventstore.GenericEventMapper[exec.SetEvent], + ), + }, + reduce: (&executionProjection{}).reduceExecutionSet, + want: wantReduce{ + aggregateType: eventstore.AggregateType("execution"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.executions (instance_id, id, resource_owner, creation_date, change_date, sequence, targets, includes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, id) DO UPDATE SET (resource_owner, creation_date, change_date, sequence, targets, includes) = (EXCLUDED.resource_owner, projections.executions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.targets, EXCLUDED.includes)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + "ro-id", + anyArg{}, + anyArg{}, + uint64(15), + []string{"target"}, + []string{"include"}, + }, + }, + }, + }, + }, + }, + { + name: "reduceExecutionRemoved", + args: args{ + event: getEvent( + testEvent( + exec.RemovedEventType, + exec.AggregateType, + []byte(`{}`), + ), + eventstore.GenericEventMapper[exec.RemovedEvent], + ), + }, + reduce: (&executionProjection{}).reduceExecutionRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("execution"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1) AND (id = $2)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceInstanceRemoved", + args: args{ + event: getEvent( + testEvent( + instance.InstanceRemovedEventType, + instance.AggregateType, + nil, + ), + instance.InstanceRemovedEventMapper, + ), + }, + reduce: reduceInstanceRemovedHelper(ExecutionInstanceIDCol), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if ok := zerrors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, ExecutionTable, tt.want) + }) + } +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 95317a7816..f6afefda24 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -74,6 +74,8 @@ var ( RestrictionsProjection *handler.Handler SystemFeatureProjection *handler.Handler InstanceFeatureProjection *handler.Handler + TargetProjection *handler.Handler + ExecutionProjection *handler.Handler ) type projection interface { @@ -152,6 +154,8 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, RestrictionsProjection = newRestrictionsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["restrictions"])) SystemFeatureProjection = newSystemFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["system_features"])) InstanceFeatureProjection = newInstanceFeatureProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_features"])) + TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"])) + ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"])) newProjectionsList() return nil } @@ -263,5 +267,7 @@ func newProjectionsList() { RestrictionsProjection, SystemFeatureProjection, InstanceFeatureProjection, + ExecutionProjection, + TargetProjection, } } diff --git a/internal/query/projection/target.go b/internal/query/projection/target.go new file mode 100644 index 0000000000..af801002a9 --- /dev/null +++ b/internal/query/projection/target.go @@ -0,0 +1,165 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/target" +) + +const ( + TargetTable = "projections.targets" + TargetIDCol = "id" + TargetCreationDateCol = "creation_date" + TargetChangeDateCol = "change_date" + TargetResourceOwnerCol = "resource_owner" + TargetInstanceIDCol = "instance_id" + TargetSequenceCol = "sequence" + TargetNameCol = "name" + TargetTargetType = "target_type" + TargetURLCol = "url" + TargetTimeoutCol = "timeout" + TargetAsyncCol = "async" + TargetInterruptOnErrorCol = "interrupt_on_error" +) + +type targetProjection struct{} + +func newTargetProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(targetProjection)) +} + +func (*targetProjection) Name() string { + return TargetTable +} + +func (*targetProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(TargetIDCol, handler.ColumnTypeText), + handler.NewColumn(TargetCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(TargetChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(TargetResourceOwnerCol, handler.ColumnTypeText), + handler.NewColumn(TargetInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(TargetTargetType, handler.ColumnTypeEnum), + handler.NewColumn(TargetSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(TargetNameCol, handler.ColumnTypeText), + handler.NewColumn(TargetURLCol, handler.ColumnTypeText, handler.Default("")), + handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64, handler.Default(0)), + handler.NewColumn(TargetAsyncCol, handler.ColumnTypeBool, handler.Default(false)), + handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool, handler.Default(false)), + }, + handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol), + ), + ) +} + +func (p *targetProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: target.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: target.AddedEventType, + Reduce: p.reduceTargetAdded, + }, + { + Event: target.ChangedEventType, + Reduce: p.reduceTargetChanged, + }, + { + Event: target.RemovedEventType, + Reduce: p.reduceTargetRemoved, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.InstanceRemovedEventType, + Reduce: reduceInstanceRemovedHelper(TargetInstanceIDCol), + }, + }, + }, + } +} + +func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*target.AddedEvent](event) + if err != nil { + return nil, err + } + return handler.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(TargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCol(TargetResourceOwnerCol, e.Aggregate().ResourceOwner), + handler.NewCol(TargetIDCol, e.Aggregate().ID), + handler.NewCol(TargetCreationDateCol, e.CreationDate()), + handler.NewCol(TargetChangeDateCol, e.CreationDate()), + handler.NewCol(TargetSequenceCol, e.Sequence()), + handler.NewCol(TargetNameCol, e.Name), + handler.NewCol(TargetURLCol, e.URL), + handler.NewCol(TargetTargetType, e.TargetType), + handler.NewCol(TargetTimeoutCol, e.Timeout), + handler.NewCol(TargetAsyncCol, e.Async), + handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError), + }, + ), nil +} + +func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*target.ChangedEvent](event) + if err != nil { + return nil, err + } + values := []handler.Column{ + handler.NewCol(TargetChangeDateCol, e.CreationDate()), + handler.NewCol(TargetSequenceCol, e.Sequence()), + handler.NewCol(TargetResourceOwnerCol, e.Aggregate().ResourceOwner), + } + if e.Name != nil { + values = append(values, handler.NewCol(TargetNameCol, *e.Name)) + } + if e.TargetType != nil { + values = append(values, handler.NewCol(TargetTargetType, *e.TargetType)) + } + if e.URL != nil { + values = append(values, handler.NewCol(TargetURLCol, *e.URL)) + } + if e.Timeout != nil { + values = append(values, handler.NewCol(TargetTimeoutCol, *e.Timeout)) + } + if e.Async != nil { + values = append(values, handler.NewCol(TargetAsyncCol, *e.Async)) + } + if e.InterruptOnError != nil { + values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError)) + } + return handler.NewUpdateStatement( + e, + values, + []handler.Condition{ + handler.NewCond(TargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(TargetIDCol, e.Aggregate().ID), + }, + ), nil +} + +func (p *targetProjection) reduceTargetRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*target.RemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewDeleteStatement( + e, + []handler.Condition{ + handler.NewCond(TargetInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(TargetIDCol, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/target_test.go b/internal/query/projection/target_test.go new file mode 100644 index 0000000000..1ba0c9379d --- /dev/null +++ b/internal/query/projection/target_test.go @@ -0,0 +1,173 @@ +package projection + +import ( + "testing" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/target" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestTargetProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceTargetAdded", + args: args{ + event: getEvent( + testEvent( + target.AddedEventType, + target.AggregateType, + []byte(`{"name": "name", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`), + ), + eventstore.GenericEventMapper[target.AddedEvent], + ), + }, + reduce: (&targetProjection{}).reduceTargetAdded, + want: wantReduce{ + aggregateType: eventstore.AggregateType("target"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO projections.targets (instance_id, resource_owner, id, creation_date, change_date, sequence, name, url, target_type, timeout, async, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + expectedArgs: []interface{}{ + "instance-id", + "ro-id", + "agg-id", + anyArg{}, + anyArg{}, + uint64(15), + "name", + "https://example.com", + domain.TargetTypeWebhook, + 3 * time.Second, + true, + true, + }, + }, + }, + }, + }, + }, + { + name: "reduceTargetChanged", + args: args{ + event: getEvent( + testEvent( + target.ChangedEventType, + target.AggregateType, + []byte(`{"name": "name2", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`), + ), + eventstore.GenericEventMapper[target.ChangedEvent], + ), + }, + reduce: (&targetProjection{}).reduceTargetChanged, + want: wantReduce{ + aggregateType: eventstore.AggregateType("target"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE projections.targets SET (change_date, sequence, resource_owner, name, target_type, url, timeout, async, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)", + expectedArgs: []interface{}{ + anyArg{}, + uint64(15), + "ro-id", + "name2", + domain.TargetTypeWebhook, + "https://example.com", + 3 * time.Second, + true, + true, + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceTargetRemoved", + args: args{ + event: getEvent( + testEvent( + target.RemovedEventType, + target.AggregateType, + []byte(`{}`), + ), + eventstore.GenericEventMapper[target.RemovedEvent], + ), + }, + reduce: (&targetProjection{}).reduceTargetRemoved, + want: wantReduce{ + aggregateType: eventstore.AggregateType("target"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1) AND (id = $2)", + expectedArgs: []interface{}{ + "instance-id", + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceInstanceRemoved", + args: args{ + event: getEvent( + testEvent( + instance.InstanceRemovedEventType, + instance.AggregateType, + nil, + ), + instance.InstanceRemovedEventMapper, + ), + }, + reduce: reduceInstanceRemovedHelper(TargetInstanceIDCol), + want: wantReduce{ + aggregateType: eventstore.AggregateType("instance"), + sequence: 15, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1)", + expectedArgs: []interface{}{ + "agg-id", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if ok := zerrors.IsErrorInvalidArgument(err); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, TargetTable, tt.want) + }) + } +} diff --git a/internal/query/target.go b/internal/query/target.go new file mode 100644 index 0000000000..ecb60dca79 --- /dev/null +++ b/internal/query/target.go @@ -0,0 +1,219 @@ +package query + +import ( + "context" + "database/sql" + "errors" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + targetTable = table{ + name: projection.TargetTable, + instanceIDCol: projection.TargetInstanceIDCol, + } + TargetColumnID = Column{ + name: projection.TargetIDCol, + table: targetTable, + } + TargetColumnCreationDate = Column{ + name: projection.TargetCreationDateCol, + table: targetTable, + } + TargetColumnChangeDate = Column{ + name: projection.TargetChangeDateCol, + table: targetTable, + } + TargetColumnResourceOwner = Column{ + name: projection.TargetResourceOwnerCol, + table: targetTable, + } + TargetColumnInstanceID = Column{ + name: projection.TargetInstanceIDCol, + table: targetTable, + } + TargetColumnSequence = Column{ + name: projection.TargetSequenceCol, + table: targetTable, + } + TargetColumnName = Column{ + name: projection.TargetNameCol, + table: targetTable, + } + TargetColumnTargetType = Column{ + name: projection.TargetTargetType, + table: targetTable, + } + TargetColumnURL = Column{ + name: projection.TargetURLCol, + table: targetTable, + } + TargetColumnTimeout = Column{ + name: projection.TargetTimeoutCol, + table: targetTable, + } + TargetColumnAsync = Column{ + name: projection.TargetAsyncCol, + table: targetTable, + } + TargetColumnInterruptOnError = Column{ + name: projection.TargetInterruptOnErrorCol, + table: targetTable, + } +) + +type Targets struct { + SearchResponse + Targets []*Target +} + +func (t *Targets) SetState(s *State) { + t.State = s +} + +type Target struct { + ID string + domain.ObjectDetails + + Name string + TargetType domain.TargetType + URL string + Timeout time.Duration + Async bool + InterruptOnError bool +} + +type TargetSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *TargetSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQueries) (targets *Targets, err error) { + eq := sq.Eq{ + TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + } + query, scan := prepareTargetsQuery(ctx, q.client) + return genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan) +} + +func (q *Queries) GetTargetByID(ctx context.Context, id string) (target *Target, err error) { + eq := sq.Eq{ + TargetColumnID.identifier(): id, + TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + } + query, scan := prepareTargetQuery(ctx, q.client) + return genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan) +} + +func NewTargetNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(TargetColumnName, value, method) +} + +func NewTargetInIDsSearchQuery(values []string) (SearchQuery, error) { + return NewInTextQuery(TargetColumnID, values) +} + +func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Targets, error)) { + return sq.Select( + TargetColumnID.identifier(), + TargetColumnChangeDate.identifier(), + TargetColumnResourceOwner.identifier(), + TargetColumnSequence.identifier(), + TargetColumnName.identifier(), + TargetColumnTargetType.identifier(), + TargetColumnTimeout.identifier(), + TargetColumnURL.identifier(), + TargetColumnAsync.identifier(), + TargetColumnInterruptOnError.identifier(), + countColumn.identifier(), + ).From(targetTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*Targets, error) { + targets := make([]*Target, 0) + var count uint64 + for rows.Next() { + target := new(Target) + err := rows.Scan( + &target.ID, + &target.EventDate, + &target.ResourceOwner, + &target.Sequence, + &target.Name, + &target.TargetType, + &target.Timeout, + &target.URL, + &target.Async, + &target.InterruptOnError, + &count, + ) + if err != nil { + return nil, err + } + targets = append(targets, target) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-fzwi6cgxos", "Errors.Query.CloseRows") + } + + return &Targets{ + Targets: targets, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Target, error)) { + return sq.Select( + TargetColumnID.identifier(), + TargetColumnChangeDate.identifier(), + TargetColumnResourceOwner.identifier(), + TargetColumnSequence.identifier(), + TargetColumnName.identifier(), + TargetColumnTargetType.identifier(), + TargetColumnTimeout.identifier(), + TargetColumnURL.identifier(), + TargetColumnAsync.identifier(), + TargetColumnInterruptOnError.identifier(), + ).From(targetTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Target, error) { + target := new(Target) + err := row.Scan( + &target.ID, + &target.EventDate, + &target.ResourceOwner, + &target.Sequence, + &target.Name, + &target.TargetType, + &target.Timeout, + &target.URL, + &target.Async, + &target.InterruptOnError, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-hj5oaniyrz", "Errors.Target.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-5qhc19sc49", "Errors.Internal") + } + return target, nil + } +} diff --git a/internal/query/target_test.go b/internal/query/target_test.go new file mode 100644 index 0000000000..d1003fc8d8 --- /dev/null +++ b/internal/query/target_test.go @@ -0,0 +1,301 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + prepareTargetsStmt = `SELECT projections.targets.id,` + + ` projections.targets.change_date,` + + ` projections.targets.resource_owner,` + + ` projections.targets.sequence,` + + ` projections.targets.name,` + + ` projections.targets.target_type,` + + ` projections.targets.timeout,` + + ` projections.targets.url,` + + ` projections.targets.async,` + + ` projections.targets.interrupt_on_error,` + + ` COUNT(*) OVER ()` + + ` FROM projections.targets` + prepareTargetsCols = []string{ + "id", + "change_date", + "resource_owner", + "sequence", + "name", + "target_type", + "timeout", + "url", + "async", + "interrupt_on_error", + "count", + } + + prepareTargetStmt = `SELECT projections.targets.id,` + + ` projections.targets.change_date,` + + ` projections.targets.resource_owner,` + + ` projections.targets.sequence,` + + ` projections.targets.name,` + + ` projections.targets.target_type,` + + ` projections.targets.timeout,` + + ` projections.targets.url,` + + ` projections.targets.async,` + + ` projections.targets.interrupt_on_error` + + ` FROM projections.targets` + prepareTargetCols = []string{ + "id", + "change_date", + "resource_owner", + "sequence", + "name", + "target_type", + "timeout", + "url", + "async", + "interrupt_on_error", + } +) + +func Test_TargetPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareTargetsQuery no result", + prepare: prepareTargetsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareTargetsStmt), + nil, + nil, + ), + }, + object: &Targets{Targets: []*Target{}}, + }, + { + name: "prepareTargetsQuery one result", + prepare: prepareTargetsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareTargetsStmt), + prepareTargetsCols, + [][]driver.Value{ + { + "id", + testNow, + "ro", + uint64(20211109), + "target-name", + domain.TargetTypeWebhook, + 1 * time.Second, + "https://example.com", + true, + true, + }, + }, + ), + }, + object: &Targets{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Targets: []*Target{ + { + ID: "id", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + }, + Name: "target-name", + TargetType: domain.TargetTypeWebhook, + Timeout: 1 * time.Second, + URL: "https://example.com", + Async: true, + InterruptOnError: true, + }, + }, + }, + }, + { + name: "prepareTargetsQuery multiple result", + prepare: prepareTargetsQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(prepareTargetsStmt), + prepareTargetsCols, + [][]driver.Value{ + { + "id-1", + testNow, + "ro", + uint64(20211109), + "target-name1", + domain.TargetTypeWebhook, + 1 * time.Second, + "https://example.com", + true, + false, + }, + { + "id-2", + testNow, + "ro", + uint64(20211110), + "target-name2", + domain.TargetTypeWebhook, + 1 * time.Second, + "https://example.com", + false, + true, + }, + }, + ), + }, + object: &Targets{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Targets: []*Target{ + { + ID: "id-1", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + }, + Name: "target-name1", + TargetType: domain.TargetTypeWebhook, + Timeout: 1 * time.Second, + URL: "https://example.com", + Async: true, + InterruptOnError: false, + }, + { + ID: "id-2", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211110, + }, + Name: "target-name2", + TargetType: domain.TargetTypeWebhook, + Timeout: 1 * time.Second, + URL: "https://example.com", + Async: false, + InterruptOnError: true, + }, + }, + }, + }, + { + name: "prepareTargetsQuery sql err", + prepare: prepareTargetsQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareTargetsStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*Target)(nil), + }, + { + name: "prepareTargetQuery no result", + prepare: prepareTargetQuery, + want: want{ + sqlExpectations: mockQueriesScanErr( + regexp.QuoteMeta(prepareTargetStmt), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !zerrors.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Target)(nil), + }, + { + name: "prepareTargetQuery found", + prepare: prepareTargetQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(prepareTargetStmt), + prepareTargetCols, + []driver.Value{ + "id", + testNow, + "ro", + uint64(20211109), + "target-name", + domain.TargetTypeWebhook, + 1 * time.Second, + "https://example.com", + true, + false, + }, + ), + }, + object: &Target{ + ID: "id", + ObjectDetails: domain.ObjectDetails{ + EventDate: testNow, + ResourceOwner: "ro", + Sequence: 20211109, + }, + Name: "target-name", + TargetType: domain.TargetTypeWebhook, + Timeout: 1 * time.Second, + URL: "https://example.com", + Async: true, + InterruptOnError: false, + }, + }, + { + name: "prepareTargetQuery sql err", + prepare: prepareTargetQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(prepareTargetStmt), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: (*Target)(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + }) + } +} diff --git a/internal/repository/execution/execution.go b/internal/repository/execution/execution.go index a03a96d8be..4c1a85a6b6 100644 --- a/internal/repository/execution/execution.go +++ b/internal/repository/execution/execution.go @@ -3,7 +3,6 @@ package execution import ( "context" - "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" ) @@ -16,9 +15,8 @@ const ( type SetEvent struct { *eventstore.BaseEvent `json:"-"` - ExecutionType domain.ExecutionType `json:"executionType"` - Targets []string `json:"targets"` - Includes []string `json:"includes"` + Targets []string `json:"targets"` + Includes []string `json:"includes"` } func (e *SetEvent) SetBaseEvent(b *eventstore.BaseEvent) { diff --git a/internal/repository/target/eventstore.go b/internal/repository/target/eventstore.go index e772e17a82..a595bde8ac 100644 --- a/internal/repository/target/eventstore.go +++ b/internal/repository/target/eventstore.go @@ -3,7 +3,7 @@ package target import "github.com/zitadel/zitadel/internal/eventstore" func init() { - eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, AddedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, ChangedEventType, ChangedEventMapper) - eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, RemovedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, eventstore.GenericEventMapper[AddedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, ChangedEventType, eventstore.GenericEventMapper[ChangedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent]) } diff --git a/internal/repository/target/target.go b/internal/repository/target/target.go index 62b7f7826c..2d50857cba 100644 --- a/internal/repository/target/target.go +++ b/internal/repository/target/target.go @@ -6,7 +6,6 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/zerrors" ) const ( @@ -17,7 +16,7 @@ const ( ) type AddedEvent struct { - *eventstore.BaseEvent `json:"-"` + eventstore.BaseEvent `json:"-"` Name string `json:"name"` TargetType domain.TargetType `json:"targetType"` @@ -28,7 +27,7 @@ type AddedEvent struct { } func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) { - e.BaseEvent = b + e.BaseEvent = *b } func (e *AddedEvent) Payload() any { @@ -50,26 +49,14 @@ func NewAddedEvent( interruptOnError bool, ) *AddedEvent { return &AddedEvent{ - eventstore.NewBaseEventForPush( + *eventstore.NewBaseEventForPush( ctx, aggregate, AddedEventType, ), name, targetType, url, timeout, async, interruptOnError} } -func AddedEventMapper(event eventstore.Event) (eventstore.Event, error) { - added := &AddedEvent{ - BaseEvent: eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(added) - if err != nil { - return nil, zerrors.ThrowInternal(err, "TARGET-fx8f8yfbn1", "unable to unmarshal target added") - } - - return added, nil -} - type ChangedEvent struct { - *eventstore.BaseEvent `json:"-"` + eventstore.BaseEvent `json:"-"` Name *string `json:"name,omitempty"` TargetType *domain.TargetType `json:"targetType,omitempty"` @@ -81,6 +68,10 @@ type ChangedEvent struct { oldName string } +func (e *ChangedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = *b +} + func (e *ChangedEvent) Payload() interface{} { return e } @@ -101,7 +92,7 @@ func NewChangedEvent( changes []Changes, ) *ChangedEvent { changeEvent := &ChangedEvent{ - BaseEvent: eventstore.NewBaseEventForPush( + BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, ChangedEventType, @@ -152,26 +143,14 @@ func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) { } } -func ChangedEventMapper(event eventstore.Event) (eventstore.Event, error) { - changed := &ChangedEvent{ - BaseEvent: eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(changed) - if err != nil { - return nil, zerrors.ThrowInternal(err, "TARGET-w6402p4ek7", "unable to unmarshal target changed") - } - - return changed, nil -} - type RemovedEvent struct { - *eventstore.BaseEvent `json:"-"` + eventstore.BaseEvent `json:"-"` name string } func (e *RemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) { - e.BaseEvent = b + e.BaseEvent = *b } func (e *RemovedEvent) Payload() any { @@ -183,17 +162,5 @@ func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { } func NewRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, name string) *RemovedEvent { - return &RemovedEvent{eventstore.NewBaseEventForPush(ctx, aggregate, RemovedEventType), name} -} - -func RemovedEventMapper(event eventstore.Event) (eventstore.Event, error) { - removed := &RemovedEvent{ - BaseEvent: eventstore.BaseEventFromRepo(event), - } - err := event.Unmarshal(removed) - if err != nil { - return nil, zerrors.ThrowInternal(err, "TARGET-0kuc12c7bc", "unable to unmarshal target removed") - } - - return removed, nil + return &RemovedEvent{*eventstore.NewBaseEventForPush(ctx, aggregate, RemovedEventType), name} } diff --git a/proto/zitadel/execution/v3alpha/execution.proto b/proto/zitadel/execution/v3alpha/execution.proto index 8a203b17ba..14d05446bd 100644 --- a/proto/zitadel/execution/v3alpha/execution.proto +++ b/proto/zitadel/execution/v3alpha/execution.proto @@ -13,23 +13,37 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha;execution"; -message SetConditions{ +message Execution { + string execution_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"request.zitadel.session.v2beta.SessionService\""; + } + ]; + // Details provide some base information (such as the last change date) of the target. + zitadel.object.v2beta.Details details = 2; + // Targets which are called in the defined conditions. + repeated string targets = 3; + // Included executions with the same condition-types. + repeated string includes = 4; +} + +message Condition { // Condition-types under which conditions the execution should happen, only one possible. oneof condition_type { option (validate.required) = true; // Condition-type to execute if a request on the defined API point happens. - SetRequestExecution request = 1; + RequestExecution request = 1; // Condition-type to execute on response if a request on the defined API point happens. - SetResponseExecution response = 2; + ResponseExecution response = 2; // Condition-type to execute if function is used, replaces actions v1. string function = 3; // Condition-type to execute if an event is created in the system. - SetEventExecution event = 4; + EventExecution event = 4; } } -message SetRequestExecution{ +message RequestExecution { // Condition for the request execution, only one possible. oneof condition{ // GRPC-method as condition. @@ -55,7 +69,7 @@ message SetRequestExecution{ } } -message SetResponseExecution{ +message ResponseExecution { // Condition for the response execution, only one possible. oneof condition{ // GRPC-method as condition. @@ -81,7 +95,7 @@ message SetResponseExecution{ } } -message SetEventExecution{ +message EventExecution{ // Condition for the event execution, only one possible. oneof condition{ // Event name as condition. diff --git a/proto/zitadel/execution/v3alpha/execution_service.proto b/proto/zitadel/execution/v3alpha/execution_service.proto index 655696008c..7c044a817d 100644 --- a/proto/zitadel/execution/v3alpha/execution_service.proto +++ b/proto/zitadel/execution/v3alpha/execution_service.proto @@ -10,6 +10,7 @@ import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "zitadel/execution/v3alpha/target.proto"; import "zitadel/execution/v3alpha/execution.proto"; +import "zitadel/execution/v3alpha/query.proto"; import "zitadel/object/v2beta/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; @@ -187,12 +188,73 @@ service ExecutionService { }; } + // List targets + // + // List all matching targets. By default, we will return all targets of your instance. + // Make sure to include a limit and sorting for pagination. + rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { + option (google.api.http) = { + post: "/v3alpha/targets/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "execution.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all targets matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Target by ID + // + // Returns the target identified by the requested ID. + rpc GetTargetByID (GetTargetByIDRequest) returns (GetTargetByIDResponse) { + option (google.api.http) = { + get: "/v3alpha/targets/{target_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "execution.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Target successfully retrieved"; + } + }; + }; + } + // Set an execution // // Set an execution to call a previously defined target or include the targets of a previously defined execution. rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { option (google.api.http) = { - post: "/v3alpha/executions" + put: "/v3alpha/executions" body: "*" }; @@ -236,6 +298,44 @@ service ExecutionService { }; }; } + + // List executions + // + // List all matching executions. By default, we will return all executions of your instance. + // Make sure to include a limit and sorting for pagination. + rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { + option (google.api.http) = { + post: "/v3alpha/executions/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "execution.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all executions matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + // List all available functions // // List all available functions which can be used as condition for executions. @@ -412,9 +512,48 @@ message DeleteTargetResponse { zitadel.object.v2beta.Details details = 1; } +message ListTargetsRequest { + // list limitations and ordering. + zitadel.object.v2beta.ListQuery query = 1; + // the field the result is sorted. + zitadel.execution.v3alpha.TargetFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"FIELD_NAME_SCHEMA_TYPE\"" + } + ]; + // Define the criteria to query for. + repeated zitadel.execution.v3alpha.TargetSearchQuery queries = 3; +} + +message ListTargetsResponse { + // Details provides information about the returned result including total amount found. + zitadel.object.v2beta.ListDetails details = 1; + // States by which field the results are sorted. + zitadel.execution.v3alpha.TargetFieldName sorting_column = 2; + // The result contains the user schemas, which matched the queries. + repeated zitadel.execution.v3alpha.Target result = 3; +} + +message GetTargetByIDRequest { + // unique identifier of the target. + string target_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetTargetByIDResponse { + zitadel.execution.v3alpha.Target target = 1; +} + message SetExecutionRequest { // Defines the condition type and content of the condition for execution. - SetConditions condition = 1; + Condition condition = 1; // Defines the execution targets which are defined as a different resource, which are called in the defined conditions. repeated string targets = 2; // Defines other executions as included with the same condition-types. @@ -428,7 +567,7 @@ message SetExecutionResponse { message DeleteExecutionRequest { // Unique identifier of the execution. - SetConditions condition = 1; + Condition condition = 1; } message DeleteExecutionResponse { @@ -436,6 +575,20 @@ message DeleteExecutionResponse { zitadel.object.v2beta.Details details = 1; } +message ListExecutionsRequest { + // list limitations and ordering. + zitadel.object.v2beta.ListQuery query = 1; + // Define the criteria to query for. + repeated zitadel.execution.v3alpha.SearchQuery queries = 2; +} + +message ListExecutionsResponse { + // Details provides information about the returned result including total amount found. + zitadel.object.v2beta.ListDetails details = 1; + // The result contains the executions, which matched the queries. + repeated zitadel.execution.v3alpha.Execution result = 2; +} + message ListExecutionFunctionsRequest{} message ListExecutionFunctionsResponse{ // All available methods diff --git a/proto/zitadel/execution/v3alpha/query.proto b/proto/zitadel/execution/v3alpha/query.proto new file mode 100644 index 0000000000..c13bd12cf8 --- /dev/null +++ b/proto/zitadel/execution/v3alpha/query.proto @@ -0,0 +1,110 @@ +syntax = "proto3"; + +package zitadel.execution.v3alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/execution/v3alpha;execution"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2beta/object.proto"; +import "zitadel/execution/v3alpha/execution.proto"; + +message SearchQuery { + oneof query { + option (validate.required) = true; + + InConditionsQuery in_conditions_query = 1; + ExecutionTypeQuery execution_type_query = 2; + TargetQuery target_query = 3; + IncludeQuery include_query = 4; + } +} + +message InConditionsQuery { + // Defines the conditions to query for. + repeated Condition conditions = 1; +} + +message ExecutionTypeQuery { + // Defines the type to query for. + ExecutionType execution_type = 1; +} + +message TargetQuery { + // Defines the id to query for. + string target_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the id of the targets to include" + example: "\"69629023906488334\""; + } + ]; +} + +message IncludeQuery { + // Defines the include to query for. + string include = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the id of the include" + example: "\"request.zitadel.session.v2beta.SessionService\""; + } + ]; +} + +message TargetSearchQuery { + oneof query { + option (validate.required) = true; + + TargetNameQuery target_name_query = 1; + InTargetIDsQuery in_target_ids_query = 2; + } +} + +message TargetNameQuery { + // Defines the name of the target to query for. + string target_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"ip_allow_list\""; + } + ]; + // Defines which text comparison method used for the name query. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + +message InTargetIDsQuery { + // Defines the ids to query for. + repeated string target_ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the ids of the targets to include" + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} + +enum ExecutionType { + EXECUTION_TYPE_UNSPECIFIED = 0; + EXECUTION_TYPE_REQUEST = 1; + EXECUTION_TYPE_RESPONSE = 2; + EXECUTION_TYPE_EVENT = 3; + EXECUTION_TYPE_FUNCTION = 4; +} + +enum TargetFieldName { + FIELD_NAME_UNSPECIFIED = 0; + FIELD_NAME_ID = 1; + FIELD_NAME_CREATION_DATE = 2; + FIELD_NAME_CHANGE_DATE = 3; + FIELD_NAME_NAME = 4; + FIELD_NAME_TARGET_TYPE = 5; + FIELD_NAME_URL = 6; + FIELD_NAME_TIMEOUT = 7; + FIELD_NAME_ASYNC = 8; + FIELD_NAME_INTERRUPT_ON_ERROR = 9; +} diff --git a/proto/zitadel/execution/v3alpha/target.proto b/proto/zitadel/execution/v3alpha/target.proto index c6991babeb..4320253e37 100644 --- a/proto/zitadel/execution/v3alpha/target.proto +++ b/proto/zitadel/execution/v3alpha/target.proto @@ -35,4 +35,39 @@ message SetRESTRequestResponse { example: "\"https://example.com/hooks/ip_check\""; } ]; +} + +message Target { + // ID is the read-only unique identifier of the target. + string target_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // Details provide some base information (such as the last change date) of the target. + zitadel.object.v2beta.Details details = 2; + + // Unique name of the target. + string name = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + SetRESTWebhook rest_webhook = 4; + SetRESTRequestResponse rest_request_response = 5; + } + // Timeout defines the duration until ZITADEL cancels the execution. + google.protobuf.Duration timeout = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + oneof execution_type { + // Set the execution to run asynchronously. + bool is_async = 7; + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 8; + } } \ No newline at end of file