mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:27:42 +00:00
feat: action v2 signing (#8779)
# Which Problems Are Solved The action v2 messages were didn't contain anything providing security for the sent content. # How the Problems Are Solved Each Target now has a SigningKey, which can also be newly generated through the API and returned at creation and through the Get-Endpoints. There is now a HTTP header "Zitadel-Signature", which is generated with the SigningKey and Payload, and also contains a timestamp to check with a tolerance if the message took to long to sent. # Additional Changes The functionality to create and check the signature is provided in the pkg/actions package, and can be reused in the SDK. # Additional Context Closes #7924 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -62,6 +62,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
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{},
|
||||
@@ -92,6 +93,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
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{},
|
||||
@@ -122,6 +124,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
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{},
|
||||
@@ -154,6 +157,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
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{},
|
||||
@@ -186,6 +190,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
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{},
|
||||
@@ -230,6 +235,7 @@ func TestServer_GetTarget(t *testing.T) {
|
||||
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")
|
||||
})
|
||||
}
|
||||
@@ -492,6 +498,7 @@ func TestServer_ListTargets(t *testing.T) {
|
||||
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)
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -200,11 +201,12 @@ func TestServer_CreateTarget(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||
assert.NotEmpty(t, got.GetSigningKey())
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -217,11 +219,15 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
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 *resource_object.Details
|
||||
want want
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
@@ -272,14 +278,42 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
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 {
|
||||
@@ -299,11 +333,13 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
want: want{
|
||||
details: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -322,11 +358,13 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
want: want{
|
||||
details: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -345,11 +383,13 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
want: want{
|
||||
details: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -370,11 +410,13 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
want: want{
|
||||
details: &resource_object.Details{
|
||||
Changed: timestamppb.Now(),
|
||||
Owner: &object.Owner{
|
||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||
Id: instance.ID(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -387,11 +429,14 @@ func TestServer_PatchTarget(t *testing.T) {
|
||||
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 {
|
||||
require.Error(t, err)
|
||||
return
|
||||
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)
|
||||
}
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -443,11 +488,12 @@ func TestServer_DeleteTarget(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -97,6 +97,7 @@ func targetToPb(t *query.Target) *action.GetTarget {
|
||||
Timeout: durationpb.New(t.Timeout),
|
||||
Endpoint: t.Endpoint,
|
||||
},
|
||||
SigningKey: t.SigningKey,
|
||||
}
|
||||
switch t.TargetType {
|
||||
case domain.TargetTypeWebhook:
|
||||
|
@@ -25,7 +25,8 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque
|
||||
return nil, err
|
||||
}
|
||||
return &action.CreateTargetResponse{
|
||||
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
|
||||
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
|
||||
SigningKey: add.SigningKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -34,12 +35,14 @@ func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest
|
||||
return nil, err
|
||||
}
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), 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),
|
||||
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
|
||||
SigningKey: patch.SigningKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -83,6 +86,12 @@ func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -91,8 +100,9 @@ func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: req.GetId(),
|
||||
},
|
||||
Name: reqTarget.Name,
|
||||
Endpoint: reqTarget.Endpoint,
|
||||
Name: reqTarget.Name,
|
||||
Endpoint: reqTarget.Endpoint,
|
||||
ExpirationSigningKey: expirationSigningKey,
|
||||
}
|
||||
if reqTarget.TargetType != nil {
|
||||
switch t := reqTarget.GetTargetType().(type) {
|
||||
|
@@ -26,6 +26,7 @@ type mockExecutionTarget struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
SigningKey string
|
||||
}
|
||||
|
||||
func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
|
||||
@@ -49,6 +50,9 @@ func (e *mockExecutionTarget) GetTargetID() string {
|
||||
func (e *mockExecutionTarget) GetExecutionID() string {
|
||||
return e.ExecutionID
|
||||
}
|
||||
func (e *mockExecutionTarget) GetSigningKey() string {
|
||||
return e.SigningKey
|
||||
}
|
||||
|
||||
type mockContentRequest struct {
|
||||
Content string
|
||||
@@ -157,6 +161,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -186,6 +191,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -216,6 +222,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -245,6 +252,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -269,6 +277,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -297,6 +306,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeAsync,
|
||||
Timeout: time.Second,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -325,6 +335,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetID: "target",
|
||||
TargetType: domain.TargetTypeAsync,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -354,6 +365,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -382,6 +394,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Second,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -411,6 +424,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -440,6 +454,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
@@ -448,6 +463,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
@@ -456,6 +472,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -498,6 +515,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
@@ -506,6 +524,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
&mockExecutionTarget{
|
||||
InstanceID: "instance",
|
||||
@@ -514,6 +533,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Second,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -692,6 +712,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
@@ -721,6 +742,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
InterruptOnError: true,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
targets: []target{
|
||||
|
Reference in New Issue
Block a user