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:
Stefan Benz
2024-11-28 11:06:52 +01:00
committed by GitHub
parent 8537805ea5
commit 7caa43ab23
37 changed files with 745 additions and 122 deletions

View File

@@ -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)

View File

@@ -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)
})
}
}

View File

@@ -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:

View File

@@ -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) {