chore!: Introduce ZITADEL v3 (#9645)

This PR summarizes multiple changes specifically only available with
ZITADEL v3:

- feat: Web Keys management
(https://github.com/zitadel/zitadel/pull/9526)
- fix(cmd): ensure proper working of mirror
(https://github.com/zitadel/zitadel/pull/9509)
- feat(Authz): system user support for permission check v2
(https://github.com/zitadel/zitadel/pull/9640)
- chore(license): change from Apache to AGPL
(https://github.com/zitadel/zitadel/pull/9597)
- feat(console): list v2 sessions
(https://github.com/zitadel/zitadel/pull/9539)
- fix(console): add loginV2 feature flag
(https://github.com/zitadel/zitadel/pull/9682)
- fix(feature flags): allow reading "own" flags
(https://github.com/zitadel/zitadel/pull/9649)
- feat(console): add Actions V2 UI
(https://github.com/zitadel/zitadel/pull/9591)

BREAKING CHANGE
- feat(webkey): migrate to v2beta API
(https://github.com/zitadel/zitadel/pull/9445)
- chore!: remove CockroachDB Support
(https://github.com/zitadel/zitadel/pull/9444)
- feat(actions): migrate to v2beta API
(https://github.com/zitadel/zitadel/pull/9489)

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
Co-authored-by: Ramon <mail@conblem.me>
Co-authored-by: Elio Bischof <elio@zitadel.com>
Co-authored-by: Kenta Yamaguchi <56732734+KEY60228@users.noreply.github.com>
Co-authored-by: Harsha Reddy <harsha.reddy@klaviyo.com>
Co-authored-by: Livio Spring <livio@zitadel.com>
Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Iraq <66622793+kkrime@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@zitadel.com>
Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Max Peintner <peintnerm@gmail.com>
This commit is contained in:
Fabienne Bühler
2025-04-02 16:53:06 +02:00
committed by GitHub
parent d14a23ae7e
commit 07ce3b6905
559 changed files with 14578 additions and 7622 deletions

View File

@@ -1,143 +0,0 @@
package action
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
reqTargets := req.GetExecution().GetTargets()
targets := make([]*execution.Target, len(reqTargets))
for i, target := range reqTargets {
switch t := target.GetType().(type) {
case *action.ExecutionTargetType_Include:
include, err := conditionToInclude(t.Include)
if err != nil {
return nil, err
}
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include}
case *action.ExecutionTargetType_Target:
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target}
}
}
set := &command.SetExecution{
Targets: targets,
}
var err error
var details *domain.ObjectDetails
instanceID := authz.GetInstance(ctx).InstanceID()
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID)
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, instanceID)
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, instanceID)
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, instanceID)
default:
err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid")
}
if err != nil {
return nil, err
}
return &action.SetExecutionResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func conditionToInclude(cond *action.Condition) (string, error) {
switch t := cond.GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(), nil
case *action.Condition_Function:
cond := command.ExecutionFunctionCondition(t.Function.GetName())
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid")
}
}
func (s *Server) ListExecutionFunctions(ctx context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
return &action.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}, nil
}
func (s *Server) ListExecutionMethods(ctx context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
return &action.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}, nil
}
func (s *Server) ListExecutionServices(ctx context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
return &action.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}, nil
}
func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: request.GetMethod(),
Service: request.GetService(),
All: request.GetAll(),
}
}
func executionConditionFromResponse(response *action.ResponseExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: response.GetMethod(),
Service: response.GetService(),
All: response.GetAll(),
}
}
func executionConditionFromEvent(event *action.EventExecution) *command.ExecutionEventCondition {
return &command.ExecutionEventCondition{
Event: event.GetEvent(),
Group: event.GetGroup(),
All: event.GetAll(),
}
}

View File

@@ -1,812 +0,0 @@
//go:build integration
package action_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}}
}
func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType {
return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}}
}
func TestServer_SetExecution_Request(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "service, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "NotExistingService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Request_Include(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
executionCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
}
instance.SetExecution(isolatedIAMOwnerCTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
)
circularExecutionService := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
}
instance.SetExecution(isolatedIAMOwnerCTX, t,
circularExecutionService,
executionTargetsSingleInclude(executionCond),
)
circularExecutionMethod := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
}
instance.SetExecution(isolatedIAMOwnerCTX, t,
circularExecutionMethod,
executionTargetsSingleInclude(circularExecutionService),
)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "method, circular error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: circularExecutionService,
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(circularExecutionMethod),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Response(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "service, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "NotExistingService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "service, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Event(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
/*
//TODO event existing check
{
name: "event, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "event, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
/*
// TODO:
{
name: "group, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "group, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "all, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Function(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "function, not existing",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "function, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "presamlresponse"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := instance.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}

View File

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

View File

@@ -1,69 +0,0 @@
//go:build integration
package action_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var (
CTX context.Context
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
CTX = ctx
return m.Run()
}())
}
func ensureFeatureEnabled(t *testing.T, instance *integration.Instance) {
ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.Actions.GetEnabled() {
return
}
_, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{
Actions: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
assert.NoError(ttt, err)
assert.True(ttt, f.Actions.GetEnabled())
},
retryDuration,
tick,
"timed out waiting for ensuring instance feature")
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute)
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
_, err := instance.Client.ActionV3Alpha.ListExecutionMethods(ctx, &action.ListExecutionMethodsRequest{})
assert.NoError(ttt, err)
},
retryDuration,
tick,
"timed out waiting for ensuring instance feature call")
}

View File

@@ -1,499 +0,0 @@
//go:build integration
package action_test
import (
"context"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_CreateTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
tests := []struct {
name string
ctx context.Context
req *action.Target
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.Target{
Name: gofakeit.Name(),
},
wantErr: true,
},
{
name: "empty name",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "webhook, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "webhook, interrupt on error, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "call, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
{
name: "call, interruptOnError, ok",
ctx: isolatedIAMOwnerCTX,
req: &action.Target{
Name: gofakeit.Name(),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
assert.NotEmpty(t, got.GetSigningKey())
}
})
}
}
func TestServer_PatchTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
type args struct {
ctx context.Context
req *action.PatchTargetRequest
}
type want struct {
details *resource_object.Details
signingKey bool
}
tests := []struct {
name string
prepare func(request *action.PatchTargetRequest) error
args args
want want
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(gofakeit.Name()),
},
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.PatchTargetRequest) error {
request.Id = "notexisting"
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(gofakeit.Name()),
},
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(gofakeit.Name()),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "regenerate signingkey, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
ExpirationSigningKey: durationpb.New(0 * time.Second),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
signingKey: true,
},
},
{
name: "change type, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "change url, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Timeout: durationpb.New(20 * time.Second),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "change type async, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
// We want to have the same response no matter how often we call the function
instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
got, err := instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.details, got.Details)
if tt.want.signingKey {
assert.NotEmpty(t, got.SigningKey)
}
}
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
instance := integration.NewInstance(CTX)
ensureFeatureEnabled(t, instance)
iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
target := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.DeleteTargetRequest
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner),
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: iamOwnerCtx,
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
}
})
}
}

View File

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

View File

@@ -1,74 +0,0 @@
package action
import (
"context"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var _ action.ZITADELActionsServer = (*Server)(nil)
type Server struct {
action.UnimplementedZITADELActionsServer
systemDefaults systemdefaults.SystemDefaults
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
ListGRPCMethods func() []string
ListGRPCServices func() []string
}
type Config struct{}
func CreateServer(
systemDefaults systemdefaults.SystemDefaults,
command *command.Commands,
query *query.Queries,
listActionFunctions func() []string,
listGRPCMethods func() []string,
listGRPCServices func() []string,
) *Server {
return &Server{
systemDefaults: systemDefaults,
command: command,
query: query,
ListActionFunctions: listActionFunctions,
ListGRPCMethods: listGRPCMethods,
ListGRPCServices: listGRPCServices,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
action.RegisterZITADELActionsServer(grpcServer, s)
}
func (s *Server) AppName() string {
return action.ZITADELActions_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ZITADELActions_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ZITADELActions_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterZITADELActionsHandler
}
func checkActionsEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().Actions {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled")
}

View File

@@ -1,124 +0,0 @@
package action
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
add := createTargetToCommand(req)
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.AddTarget(ctx, add, instanceID)
if err != nil {
return nil, err
}
return &action.CreateTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SigningKey: add.SigningKey,
}, nil
}
func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
patch := patchTargetToCommand(req)
details, err := s.command.ChangeTarget(ctx, patch, instanceID)
if err != nil {
return nil, err
}
return &action.PatchTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SigningKey: patch.SigningKey,
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) {
if err := checkActionsEnabled(ctx); err != nil {
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID)
if err != nil {
return nil, err
}
return &action.DeleteTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
}, nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
reqTarget := req.GetTarget()
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := reqTarget.GetTargetType().(type) {
case *action.Target_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.Target_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.Target_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: reqTarget.GetName(),
TargetType: targetType,
Endpoint: reqTarget.GetEndpoint(),
Timeout: reqTarget.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget {
expirationSigningKey := false
// TODO handle expiration, currently only immediate expiration is supported
if req.GetTarget().GetExpirationSigningKey() != nil {
expirationSigningKey = true
}
reqTarget := req.GetTarget()
if reqTarget == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: reqTarget.Name,
Endpoint: reqTarget.Endpoint,
ExpirationSigningKey: expirationSigningKey,
}
if reqTarget.TargetType != nil {
switch t := reqTarget.GetTargetType().(type) {
case *action.PatchTarget_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.PatchTarget_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.PatchTarget_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if reqTarget.Timeout != nil {
target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration())
}
return target
}

View File

@@ -1,229 +0,0 @@
package action
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.Target
}
tests := []struct {
name string
args args
want *command.AddTarget
}{
{
name: "nil",
args: args{nil},
want: &command.AddTarget{
Name: "",
Endpoint: "",
Timeout: 0,
InterruptOnError: false,
},
},
{
name: "all fields (webhook)",
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (async)",
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeAsync,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (interrupting response)",
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeCall,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req})
assert.Equal(t, tt.want, got)
})
}
}
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.PatchTarget
}
tests := []struct {
name string
args args
want *command.ChangeTarget
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields nil",
args: args{&action.PatchTarget{
Name: nil,
TargetType: nil,
Timeout: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
Endpoint: nil,
Timeout: nil,
InterruptOnError: nil,
},
},
{
name: "all fields empty",
args: args{&action.PatchTarget{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
Endpoint: nil,
Timeout: gu.Ptr(0 * time.Second),
InterruptOnError: nil,
},
},
{
name: "all fields (webhook)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (webhook interrupt)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
{
name: "all fields (async)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeAsync),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (interrupting response)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req})
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -1,253 +0,0 @@
//go:build integration
package webkey_test
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
var (
CTX context.Context
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
CTX = ctx
return m.Run()
}())
}
func TestServer_Feature_Disabled(t *testing.T) {
instance, iamCtx, _ := createInstance(t, false)
client := instance.Client.WebKeyV3Alpha
t.Run("CreateWebKey", func(t *testing.T) {
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{})
assertFeatureDisabledError(t, err)
})
t.Run("ActivateWebKey", func(t *testing.T) {
_, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: "1",
})
assertFeatureDisabledError(t, err)
})
t.Run("DeleteWebKey", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: "1",
})
assertFeatureDisabledError(t, err)
})
t.Run("ListWebKeys", func(t *testing.T) {
_, err := client.ListWebKeys(iamCtx, &webkey.ListWebKeysRequest{})
assertFeatureDisabledError(t, err)
})
}
func TestServer_ListWebKeys(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
// After the feature is first enabled, we can expect 2 generated keys with the default config.
checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
}, creationDate)
}
func TestServer_CreateWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
client := instance.Client.WebKeyV3Alpha
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
},
})
require.NoError(t, err)
checkWebKeyListState(iamCtx, t, instance, 3, "", &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
}, creationDate)
}
func TestServer_ActivateWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
client := instance.Client.WebKeyV3Alpha
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
},
})
require.NoError(t, err)
_, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: resp.GetDetails().GetId(),
})
require.NoError(t, err)
checkWebKeyListState(iamCtx, t, instance, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
}, creationDate)
}
func TestServer_DeleteWebKey(t *testing.T) {
instance, iamCtx, creationDate := createInstance(t, true)
client := instance.Client.WebKeyV3Alpha
keyIDs := make([]string, 2)
for i := 0; i < 2; i++ {
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
},
})
require.NoError(t, err)
keyIDs[i] = resp.GetDetails().GetId()
}
_, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: keyIDs[0],
})
require.NoError(t, err)
ok := t.Run("cannot delete active key", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: keyIDs[0],
})
require.Error(t, err)
s := status.Convert(err)
assert.Equal(t, codes.FailedPrecondition, s.Code())
assert.Contains(t, s.Message(), "COMMAND-Chai1")
})
if !ok {
return
}
ok = t.Run("delete inactive key", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: keyIDs[1],
})
require.NoError(t, err)
})
if !ok {
return
}
// There are 2 keys from feature setup, +2 created, -1 deleted = 3
checkWebKeyListState(iamCtx, t, instance, 3, keyIDs[0], &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
}, creationDate)
}
func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, context.Context, *timestamppb.Timestamp) {
instance := integration.NewInstance(CTX)
creationDate := timestamppb.Now()
iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
if enableFeature {
_, err := instance.Client.FeatureV2.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{
WebKey: proto.Bool(true),
})
require.NoError(t, err)
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
if enableFeature {
assert.NoError(collect, err)
assert.Len(collect, resp.GetWebKeys(), 2)
} else {
assert.Error(collect, err)
}
}, retryDuration, tick)
return instance, iamCTX, creationDate
}
func assertFeatureDisabledError(t *testing.T, err error) {
t.Helper()
require.Error(t, err)
s := status.Convert(err)
assert.Equal(t, codes.FailedPrecondition, s.Code())
assert.Contains(t, s.Message(), "WEBKEY-Ohx6E")
}
func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integration.Instance, nKeys int, expectActiveKeyID string, config any, creationDate *timestamppb.Timestamp) {
t.Helper()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
resp, err := instance.Client.WebKeyV3Alpha.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
require.NoError(collect, err)
list := resp.GetWebKeys()
assert.Len(collect, list, nKeys)
now := time.Now()
var gotActiveKeyID string
for _, key := range list {
integration.AssertResourceDetails(t, &resource_object.Details{
Created: creationDate,
Changed: creationDate,
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
}, key.GetDetails())
assert.WithinRange(collect, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEqual(collect, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState())
assert.NotEqual(collect, webkey.WebKeyState_STATE_REMOVED, key.GetState())
assert.Equal(collect, config, key.GetConfig().GetConfig())
if key.GetState() == webkey.WebKeyState_STATE_ACTIVE {
gotActiveKeyID = key.GetDetails().GetId()
}
}
assert.NotEmpty(collect, gotActiveKeyID)
if expectActiveKeyID != "" {
assert.Equal(collect, expectActiveKeyID, gotActiveKeyID)
}
}, retryDuration, tick)
}

View File

@@ -1,47 +0,0 @@
package webkey
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
type Server struct {
webkey.UnimplementedZITADELWebKeysServer
command *command.Commands
query *query.Queries
}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
webkey.RegisterZITADELWebKeysServer(grpcServer, s)
}
func (s *Server) AppName() string {
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return webkey.ZITADELWebKeys_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return webkey.RegisterZITADELWebKeysHandler
}

View File

@@ -1,87 +0,0 @@
package webkey
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req))
if err != nil {
return nil, err
}
return &webkey.CreateWebKeyResponse{
Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyRequest) (_ *webkey.ActivateWebKeyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
details, err := s.command.ActivateWebKey(ctx, req.GetId())
if err != nil {
return nil, err
}
return &webkey.ActivateWebKeyResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyRequest) (_ *webkey.DeleteWebKeyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteWebKey(ctx, req.GetId())
if err != nil {
return nil, err
}
return &webkey.DeleteWebKeyResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) (_ *webkey.ListWebKeysResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
list, err := s.query.ListWebKeys(ctx)
if err != nil {
return nil, err
}
return &webkey.ListWebKeysResponse{
WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func checkWebKeyFeature(ctx context.Context) error {
if !authz.GetFeatures(ctx).WebKey {
return zerrors.ThrowPreconditionFailed(nil, "WEBKEY-Ohx6E", "Errors.WebKey.FeatureDisabled")
}
return nil
}

View File

@@ -1,173 +0,0 @@
package webkey
import (
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
switch config := req.GetKey().GetConfig().(type) {
case *webkey.WebKey_Rsa:
return webKeyRSAConfigToCrypto(config.Rsa)
case *webkey.WebKey_Ecdsa:
return webKeyECDSAConfigToCrypto(config.Ecdsa)
case *webkey.WebKey_Ed25519:
return new(crypto.WebKeyED25519Config)
default:
return webKeyRSAConfigToCrypto(nil)
}
}
func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig {
out := new(crypto.WebKeyRSAConfig)
switch config.GetBits() {
case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED:
out.Bits = crypto.RSABits2048
case webkey.WebKeyRSAConfig_RSA_BITS_2048:
out.Bits = crypto.RSABits2048
case webkey.WebKeyRSAConfig_RSA_BITS_3072:
out.Bits = crypto.RSABits3072
case webkey.WebKeyRSAConfig_RSA_BITS_4096:
out.Bits = crypto.RSABits4096
default:
out.Bits = crypto.RSABits2048
}
switch config.GetHasher() {
case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED:
out.Hasher = crypto.RSAHasherSHA256
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256:
out.Hasher = crypto.RSAHasherSHA256
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384:
out.Hasher = crypto.RSAHasherSHA384
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512:
out.Hasher = crypto.RSAHasherSHA512
default:
out.Hasher = crypto.RSAHasherSHA256
}
return out
}
func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig {
out := new(crypto.WebKeyECDSAConfig)
switch config.GetCurve() {
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED:
out.Curve = crypto.EllipticCurveP256
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256:
out.Curve = crypto.EllipticCurveP256
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384:
out.Curve = crypto.EllipticCurveP384
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512:
out.Curve = crypto.EllipticCurveP512
default:
out.Curve = crypto.EllipticCurveP256
}
return out
}
func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey {
out := make([]*webkey.GetWebKey, len(list))
for i := range list {
out[i] = webKeyDetailsToPb(&list[i], instanceID)
}
return out
}
func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey {
out := &webkey.GetWebKey{
Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{
ID: details.KeyID,
CreationDate: details.CreationDate,
EventDate: details.ChangeDate,
}, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
State: webKeyStateToPb(details.State),
Config: &webkey.WebKey{},
}
switch config := details.Config.(type) {
case *crypto.WebKeyRSAConfig:
out.Config.Config = &webkey.WebKey_Rsa{
Rsa: webKeyRSAConfigToPb(config),
}
case *crypto.WebKeyECDSAConfig:
out.Config.Config = &webkey.WebKey_Ecdsa{
Ecdsa: webKeyECDSAConfigToPb(config),
}
case *crypto.WebKeyED25519Config:
out.Config.Config = &webkey.WebKey_Ed25519{
Ed25519: new(webkey.WebKeyED25519Config),
}
}
return out
}
func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState {
switch state {
case domain.WebKeyStateUnspecified:
return webkey.WebKeyState_STATE_UNSPECIFIED
case domain.WebKeyStateInitial:
return webkey.WebKeyState_STATE_INITIAL
case domain.WebKeyStateActive:
return webkey.WebKeyState_STATE_ACTIVE
case domain.WebKeyStateInactive:
return webkey.WebKeyState_STATE_INACTIVE
case domain.WebKeyStateRemoved:
return webkey.WebKeyState_STATE_REMOVED
default:
return webkey.WebKeyState_STATE_UNSPECIFIED
}
}
func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig {
out := new(webkey.WebKeyRSAConfig)
switch config.Bits {
case crypto.RSABitsUnspecified:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED
case crypto.RSABits2048:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048
case crypto.RSABits3072:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072
case crypto.RSABits4096:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096
}
switch config.Hasher {
case crypto.RSAHasherUnspecified:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED
case crypto.RSAHasherSHA256:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256
case crypto.RSAHasherSHA384:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384
case crypto.RSAHasherSHA512:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512
}
return out
}
func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig {
out := new(webkey.WebKeyECDSAConfig)
switch config.Curve {
case crypto.EllipticCurveUnspecified:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED
case crypto.EllipticCurveP256:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256
case crypto.EllipticCurveP384:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384
case crypto.EllipticCurveP512:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512
}
return out
}

View File

@@ -1,529 +0,0 @@
package webkey
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func Test_createWebKeyRequestToConfig(t *testing.T) {
type args struct {
req *webkey.CreateWebKeyRequest
}
tests := []struct {
name string
args args
want crypto.WebKeyConfig
}{
{
name: "RSA",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
},
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
},
{
name: "ECDSA",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
},
},
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
{
name: "ED25519",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
},
}},
want: &crypto.WebKeyED25519Config{},
},
{
name: "default",
args: args{&webkey.CreateWebKeyRequest{}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createWebKeyRequestToConfig(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyRSAConfigToCrypto(t *testing.T) {
type args struct {
config *webkey.WebKeyRSAConfig
}
tests := []struct {
name string
args args
want *crypto.WebKeyRSAConfig
}{
{
name: "unspecified",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
{
name: "2048, RSA256",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
{
name: "3072, RSA384",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
},
{
name: "4096, RSA512",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits4096,
Hasher: crypto.RSAHasherSHA512,
},
},
{
name: "invalid",
args: args{&webkey.WebKeyRSAConfig{
Bits: 99,
Hasher: 99,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyRSAConfigToCrypto(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
type args struct {
config *webkey.WebKeyECDSAConfig
}
tests := []struct {
name string
args args
want *crypto.WebKeyECDSAConfig
}{
{
name: "unspecified",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
},
},
{
name: "P256",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
},
},
{
name: "P384",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
{
name: "P512",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP512,
},
},
{
name: "invalid",
args: args{&webkey.WebKeyECDSAConfig{
Curve: 99,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyECDSAConfigToCrypto(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyDetailsListToPb(t *testing.T) {
instanceID := "ownerid"
list := []query.WebKeyDetails{
{
KeyID: "key1",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
},
{
KeyID: "key2",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyED25519Config{},
},
}
want := []*webkey.GetWebKey{
{
Details: &resource_object.Details{
Id: "key1",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
},
},
{
Details: &resource_object.Details{
Id: "key2",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
},
},
}
got := webKeyDetailsListToPb(list, instanceID)
assert.Equal(t, want, got)
}
func Test_webKeyDetailsToPb(t *testing.T) {
instanceID := "ownerid"
type args struct {
details *query.WebKeyDetails
}
tests := []struct {
name string
args args
want *webkey.GetWebKey
}{
{
name: "RSA",
args: args{&query.WebKeyDetails{
KeyID: "keyID",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
},
},
},
{
name: "ECDSA",
args: args{&query.WebKeyDetails{
KeyID: "keyID",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
},
},
},
},
{
name: "ED25519",
args: args{&query.WebKeyDetails{
KeyID: "keyID",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyED25519Config{},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyDetailsToPb(tt.args.details, instanceID)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyStateToPb(t *testing.T) {
type args struct {
state domain.WebKeyState
}
tests := []struct {
name string
args args
want webkey.WebKeyState
}{
{
name: "unspecified",
args: args{domain.WebKeyStateUnspecified},
want: webkey.WebKeyState_STATE_UNSPECIFIED,
},
{
name: "initial",
args: args{domain.WebKeyStateInitial},
want: webkey.WebKeyState_STATE_INITIAL,
},
{
name: "active",
args: args{domain.WebKeyStateActive},
want: webkey.WebKeyState_STATE_ACTIVE,
},
{
name: "inactive",
args: args{domain.WebKeyStateInactive},
want: webkey.WebKeyState_STATE_INACTIVE,
},
{
name: "removed",
args: args{domain.WebKeyStateRemoved},
want: webkey.WebKeyState_STATE_REMOVED,
},
{
name: "invalid",
args: args{99},
want: webkey.WebKeyState_STATE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyStateToPb(tt.args.state)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyRSAConfigToPb(t *testing.T) {
type args struct {
config *crypto.WebKeyRSAConfig
}
tests := []struct {
name string
args args
want *webkey.WebKeyRSAConfig
}{
{
name: "2048, RSA256",
args: args{&crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
{
name: "3072, RSA384",
args: args{&crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
{
name: "4096, RSA512",
args: args{&crypto.WebKeyRSAConfig{
Bits: crypto.RSABits4096,
Hasher: crypto.RSAHasherSHA512,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyRSAConfigToPb(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyECDSAConfigToPb(t *testing.T) {
type args struct {
config *crypto.WebKeyECDSAConfig
}
tests := []struct {
name string
args args
want *webkey.WebKeyECDSAConfig
}{
{
name: "P256",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
},
},
{
name: "P384",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
},
{
name: "P512",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP512,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyECDSAConfigToPb(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}