mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 15:35:10 +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:
parent
8537805ea5
commit
7caa43ab23
@ -633,6 +633,9 @@ EncryptionKeys:
|
||||
User:
|
||||
EncryptionKeyID: "userKey" # ZITADEL_ENCRYPTIONKEYS_USER_ENCRYPTIONKEYID
|
||||
DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_USER_DECRYPTIONKEYIDS (comma separated list)
|
||||
Target:
|
||||
EncryptionKeyID: "targetKey" # ZITADEL_ENCRYPTIONKEYS_TARGET_ENCRYPTIONKEYID
|
||||
DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_TARGET_DECRYPTIONKEYIDS (comma separated list)
|
||||
CSRFCookieKeyID: "csrfCookieKey" # ZITADEL_ENCRYPTIONKEYS_CSRFCOOKIEKEYID
|
||||
UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID
|
||||
|
||||
@ -910,6 +913,12 @@ DefaultInstance:
|
||||
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEUPPERLETTERS
|
||||
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEDIGITS
|
||||
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDESYMBOLS
|
||||
SigningKey:
|
||||
Length: 36 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_LENGTH
|
||||
IncludeLowerLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDELOWERLETTERS
|
||||
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDEUPPERLETTERS
|
||||
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDEDIGITS
|
||||
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDESYMBOLS
|
||||
PasswordComplexityPolicy:
|
||||
MinLength: 8 # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_MINLENGTH
|
||||
HasLowercase: true # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_HASLOWERCASE
|
||||
|
@ -17,6 +17,7 @@ var (
|
||||
"smsKey",
|
||||
"smtpKey",
|
||||
"userKey",
|
||||
"targetKey",
|
||||
"csrfCookieKey",
|
||||
"userAgentCookieKey",
|
||||
}
|
||||
@ -31,6 +32,7 @@ type EncryptionKeyConfig struct {
|
||||
SMS *crypto.KeyConfig
|
||||
SMTP *crypto.KeyConfig
|
||||
User *crypto.KeyConfig
|
||||
Target *crypto.KeyConfig
|
||||
CSRFCookieKeyID string
|
||||
UserAgentCookieKeyID string
|
||||
}
|
||||
@ -44,6 +46,7 @@ type EncryptionKeys struct {
|
||||
SMS crypto.EncryptionAlgorithm
|
||||
SMTP crypto.EncryptionAlgorithm
|
||||
User crypto.EncryptionAlgorithm
|
||||
Target crypto.EncryptionAlgorithm
|
||||
CSRFCookieKey []byte
|
||||
UserAgentCookieKey []byte
|
||||
OIDCKey []byte
|
||||
@ -91,6 +94,10 @@ func EnsureEncryptionKeys(ctx context.Context, keyConfig *EncryptionKeyConfig, k
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys.Target, err = crypto.NewAESCrypto(keyConfig.Target, keyStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err = crypto.LoadKey(keyConfig.CSRFCookieKeyID, keyStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -145,6 +145,7 @@ func projections(
|
||||
keys.OTP,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
config.InternalAuthZ.RolePermissionMappings,
|
||||
sessionTokenVerifier,
|
||||
func(q *query.Queries) domain.PermissionCheck {
|
||||
@ -183,6 +184,7 @@ func projections(
|
||||
keys.DomainVerification,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
&http.Client{},
|
||||
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
|
||||
|
@ -86,6 +86,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@ -53,6 +53,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@ -366,6 +366,7 @@ func initProjections(
|
||||
keys.OTP,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
config.InternalAuthZ.RolePermissionMappings,
|
||||
sessionTokenVerifier,
|
||||
func(q *query.Queries) domain.PermissionCheck {
|
||||
@ -422,6 +423,7 @@ func initProjections(
|
||||
keys.DomainVerification,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
&http.Client{},
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
|
@ -196,6 +196,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys.OTP,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
config.InternalAuthZ.RolePermissionMappings,
|
||||
sessionTokenVerifier,
|
||||
func(q *query.Queries) domain.PermissionCheck {
|
||||
@ -245,6 +246,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys.DomainVerification,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
&http.Client{},
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
|
@ -48,6 +48,20 @@ func main() {
|
||||
|
||||
What happens here is only a target which prints out the received request, which could also be handled with a different logic.
|
||||
|
||||
### Check Signature
|
||||
|
||||
To additionally check the signature header you can add the following to the example:
|
||||
```go
|
||||
// validate signature
|
||||
if err := actions.ValidatePayload(sentBody, req.Header.Get(actions.SigningHeader), signingKey); err != nil {
|
||||
// if the signed content is not equal the sent content return an error
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
Where you can replace 'signingKey' with the key received in the next step 'Create target'.
|
||||
|
||||
## Create target
|
||||
|
||||
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:
|
||||
|
@ -64,6 +64,13 @@ There are different types of Targets:
|
||||
|
||||
The API documentation to create a target can be found [here](/apis/resources/action_service_v3/zitadel-actions-create-target)
|
||||
|
||||
### Content Signing
|
||||
|
||||
To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical.
|
||||
|
||||
Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v3/zitadel-actions-create-target),
|
||||
and can also be newly generated when a Target is [patched](/apis/resources/action_service_v3/zitadel-actions-patch-target).
|
||||
|
||||
## Execution
|
||||
|
||||
ZITADEL decides on specific conditions if one or more Targets have to be called.
|
||||
|
@ -17,7 +17,7 @@
|
||||
"generate:grpc": "buf generate ../proto",
|
||||
"generate:apidocs": "docusaurus gen-api-docs all",
|
||||
"generate:configdocs": "cp -r ../cmd/defaults.yaml ./docs/self-hosting/manage/configure/ && cp -r ../cmd/setup/steps.yaml ./docs/self-hosting/manage/configure/",
|
||||
"generate:re-gen": "yarn clean-all && yarn gen-all",
|
||||
"generate:re-gen": "yarn generate:clean-all && yarn generate",
|
||||
"generate:clean-all": "docusaurus clean-api-docs all"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -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{
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/execution"
|
||||
@ -172,6 +173,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -221,6 +228,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -270,6 +283,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -836,6 +855,12 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
),
|
||||
),
|
||||
expectPushFailed(
|
||||
@ -930,6 +955,12 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
@ -19,6 +21,8 @@ type AddTarget struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
|
||||
SigningKey string
|
||||
}
|
||||
|
||||
func (a *AddTarget) IsValid() error {
|
||||
@ -58,7 +62,11 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
|
||||
if wm.State.Exists() {
|
||||
return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists")
|
||||
}
|
||||
|
||||
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
add.SigningKey = code.PlainCode()
|
||||
pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent(
|
||||
ctx,
|
||||
TargetAggregateFromWriteModel(&wm.WriteModel),
|
||||
@ -67,6 +75,7 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
|
||||
add.Endpoint,
|
||||
add.Timeout,
|
||||
add.InterruptOnError,
|
||||
code.Crypted,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -85,6 +94,9 @@ type ChangeTarget struct {
|
||||
Endpoint *string
|
||||
Timeout *time.Duration
|
||||
InterruptOnError *bool
|
||||
|
||||
ExpirationSigningKey bool
|
||||
SigningKey *string
|
||||
}
|
||||
|
||||
func (a *ChangeTarget) IsValid() error {
|
||||
@ -120,6 +132,17 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
|
||||
if !existing.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound")
|
||||
}
|
||||
|
||||
var changedSigningKey *crypto.CryptoValue
|
||||
if change.ExpirationSigningKey {
|
||||
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changedSigningKey = code.Crypted
|
||||
change.SigningKey = &code.Plain
|
||||
}
|
||||
|
||||
changedEvent := existing.NewChangedEvent(
|
||||
ctx,
|
||||
TargetAggregateFromWriteModel(&existing.WriteModel),
|
||||
@ -127,7 +150,9 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
|
||||
change.TargetType,
|
||||
change.Endpoint,
|
||||
change.Timeout,
|
||||
change.InterruptOnError)
|
||||
change.InterruptOnError,
|
||||
changedSigningKey,
|
||||
)
|
||||
if changedEvent == nil {
|
||||
return writeModelToObjectDetails(&existing.WriteModel), nil
|
||||
}
|
||||
@ -184,3 +209,7 @@ func (c *Commands) getTargetWriteModelByID(ctx context.Context, id string, resou
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
func (c *Commands) newSigningKey(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) {
|
||||
return c.newEncryptedCodeWithDefault(ctx, filter, domain.SecretGeneratorTypeSigningKey, alg, c.defaultSecretGenerators.SigningKey)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
@ -18,6 +19,7 @@ type TargetWriteModel struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
SigningKey *crypto.CryptoValue
|
||||
|
||||
State domain.TargetState
|
||||
}
|
||||
@ -41,6 +43,7 @@ func (wm *TargetWriteModel) Reduce() error {
|
||||
wm.Endpoint = e.Endpoint
|
||||
wm.Timeout = e.Timeout
|
||||
wm.State = domain.TargetActive
|
||||
wm.SigningKey = e.SigningKey
|
||||
case *target.ChangedEvent:
|
||||
if e.Name != nil {
|
||||
wm.Name = *e.Name
|
||||
@ -57,6 +60,9 @@ func (wm *TargetWriteModel) Reduce() error {
|
||||
if e.InterruptOnError != nil {
|
||||
wm.InterruptOnError = *e.InterruptOnError
|
||||
}
|
||||
if e.SigningKey != nil {
|
||||
wm.SigningKey = e.SigningKey
|
||||
}
|
||||
case *target.RemovedEvent:
|
||||
wm.State = domain.TargetRemoved
|
||||
}
|
||||
@ -84,6 +90,7 @@ func (wm *TargetWriteModel) NewChangedEvent(
|
||||
endpoint *string,
|
||||
timeout *time.Duration,
|
||||
interruptOnError *bool,
|
||||
signingKey *crypto.CryptoValue,
|
||||
) *target.ChangedEvent {
|
||||
changes := make([]target.Changes, 0)
|
||||
if name != nil && wm.Name != *name {
|
||||
@ -101,6 +108,10 @@ func (wm *TargetWriteModel) NewChangedEvent(
|
||||
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
|
||||
changes = append(changes, target.ChangeInterruptOnError(*interruptOnError))
|
||||
}
|
||||
// if signingkey is set, update it as it is encrypted
|
||||
if signingKey != nil {
|
||||
changes = append(changes, target.ChangeSigningKey(signingKey))
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/target"
|
||||
@ -20,6 +21,12 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
false,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@ -19,8 +20,10 @@ import (
|
||||
|
||||
func TestCommands_AddTarget(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||
defaultSecretGenerators *SecretGenerators
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -132,10 +135,18 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
"https://example.com",
|
||||
time.Second,
|
||||
false,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id1"),
|
||||
idGenerator: mock.ExpectID(t, "id1"),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
@ -186,7 +197,9 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
targetAddEvent("id1", "instance"),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id1"),
|
||||
idGenerator: mock.ExpectID(t, "id1"),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
@ -219,7 +232,9 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
}(),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id1"),
|
||||
idGenerator: mock.ExpectID(t, "id1"),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
@ -244,8 +259,10 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
|
||||
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
|
||||
}
|
||||
details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
|
||||
if tt.res.err == nil {
|
||||
@ -264,7 +281,9 @@ func TestCommands_AddTarget(t *testing.T) {
|
||||
|
||||
func TestCommands_ChangeTarget(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||
defaultSecretGenerators *SecretGenerators
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -510,10 +529,18 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
||||
target.ChangeTargetType(domain.TargetTypeCall),
|
||||
target.ChangeTimeout(10 * time.Second),
|
||||
target.ChangeInterruptOnError(true),
|
||||
target.ChangeSigningKey(&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("12345678"),
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
|
||||
defaultSecretGenerators: &SecretGenerators{},
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
@ -521,11 +548,12 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "id1",
|
||||
},
|
||||
Name: gu.Ptr("name2"),
|
||||
Endpoint: gu.Ptr("https://example2.com"),
|
||||
TargetType: gu.Ptr(domain.TargetTypeCall),
|
||||
Timeout: gu.Ptr(10 * time.Second),
|
||||
InterruptOnError: gu.Ptr(true),
|
||||
Name: gu.Ptr("name2"),
|
||||
Endpoint: gu.Ptr("https://example2.com"),
|
||||
TargetType: gu.Ptr(domain.TargetTypeCall),
|
||||
Timeout: gu.Ptr(10 * time.Second),
|
||||
InterruptOnError: gu.Ptr(true),
|
||||
ExpirationSigningKey: true,
|
||||
},
|
||||
resourceOwner: "instance",
|
||||
},
|
||||
@ -540,7 +568,9 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
|
||||
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
|
||||
}
|
||||
details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
|
||||
if tt.res.err == nil {
|
||||
|
@ -54,6 +54,7 @@ type Commands struct {
|
||||
smtpEncryption crypto.EncryptionAlgorithm
|
||||
smsEncryption crypto.EncryptionAlgorithm
|
||||
userEncryption crypto.EncryptionAlgorithm
|
||||
targetEncryption crypto.EncryptionAlgorithm
|
||||
userPasswordHasher *crypto.Hasher
|
||||
secretHasher *crypto.Hasher
|
||||
machineKeySize int
|
||||
@ -108,7 +109,7 @@ func StartCommands(
|
||||
externalDomain string,
|
||||
externalSecure bool,
|
||||
externalPort uint16,
|
||||
idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption crypto.EncryptionAlgorithm,
|
||||
idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption, targetEncryption crypto.EncryptionAlgorithm,
|
||||
httpClient *http.Client,
|
||||
permissionCheck domain.PermissionCheck,
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||
@ -153,6 +154,7 @@ func StartCommands(
|
||||
smtpEncryption: smtpEncryption,
|
||||
smsEncryption: smsEncryption,
|
||||
userEncryption: userEncryption,
|
||||
targetEncryption: targetEncryption,
|
||||
userPasswordHasher: userPasswordHasher,
|
||||
secretHasher: secretHasher,
|
||||
machineKeySize: int(defaults.SecretGenerators.MachineKeySize),
|
||||
|
@ -157,6 +157,7 @@ type SecretGenerators struct {
|
||||
OTPSMS *crypto.GeneratorConfig
|
||||
OTPEmail *crypto.GeneratorConfig
|
||||
InviteCode *crypto.GeneratorConfig
|
||||
SigningKey *crypto.GeneratorConfig
|
||||
}
|
||||
|
||||
type ZitadelConfig struct {
|
||||
|
@ -15,6 +15,7 @@ const (
|
||||
SecretGeneratorTypeOTPSMS
|
||||
SecretGeneratorTypeOTPEmail
|
||||
SecretGeneratorTypeInviteCode
|
||||
SecretGeneratorTypeSigningKey
|
||||
|
||||
secretGeneratorTypeCount
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
"github.com/zitadel/zitadel/pkg/actions"
|
||||
)
|
||||
|
||||
type ContextInfo interface {
|
||||
@ -28,6 +29,7 @@ type Target interface {
|
||||
GetEndpoint() string
|
||||
GetTargetType() domain.TargetType
|
||||
GetTimeout() time.Duration
|
||||
GetSigningKey() string
|
||||
}
|
||||
|
||||
// CallTargets call a list of targets in order with handling of error and responses
|
||||
@ -72,13 +74,13 @@ func CallTarget(
|
||||
switch target.GetTargetType() {
|
||||
// get request, ignore response and return request and error for handling in list of targets
|
||||
case domain.TargetTypeWebhook:
|
||||
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
|
||||
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey())
|
||||
// get request, return response and error
|
||||
case domain.TargetTypeCall:
|
||||
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
|
||||
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey())
|
||||
case domain.TargetTypeAsync:
|
||||
go func(target Target, info ContextInfoRequest) {
|
||||
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
|
||||
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()); err != nil {
|
||||
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
|
||||
}
|
||||
}(target, info)
|
||||
@ -89,13 +91,13 @@ func CallTarget(
|
||||
}
|
||||
|
||||
// webhook call a webhook, ignore the response but return the errror
|
||||
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error {
|
||||
_, err := Call(ctx, url, timeout, body)
|
||||
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) error {
|
||||
_, err := Call(ctx, url, timeout, body, signingKey)
|
||||
return err
|
||||
}
|
||||
|
||||
// Call function to do a post HTTP request to a desired url with timeout
|
||||
func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) {
|
||||
func Call(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) (_ []byte, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() {
|
||||
@ -108,6 +110,9 @@ func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if signingKey != "" {
|
||||
req.Header.Set(actions.SigningHeader, actions.ComputeSignatureHeader(time.Now(), body, signingKey))
|
||||
}
|
||||
|
||||
client := http.DefaultClient
|
||||
resp, err := client.Do(req)
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
"github.com/zitadel/zitadel/pkg/actions"
|
||||
)
|
||||
|
||||
func Test_Call(t *testing.T) {
|
||||
@ -29,6 +30,7 @@ func Test_Call(t *testing.T) {
|
||||
body []byte
|
||||
respBody []byte
|
||||
statusCode int
|
||||
signingKey string
|
||||
}
|
||||
type res struct {
|
||||
body []byte
|
||||
@ -84,6 +86,22 @@ func Test_Call(t *testing.T) {
|
||||
body: []byte("{\"response\": \"values\"}"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"ok, signed",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
timeout: time.Minute,
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
body: []byte("{\"request\": \"values\"}"),
|
||||
respBody: []byte("{\"response\": \"values\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
},
|
||||
res{
|
||||
body: []byte("{\"response\": \"values\"}"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -95,7 +113,7 @@ func Test_Call(t *testing.T) {
|
||||
statusCode: tt.args.statusCode,
|
||||
respondBody: tt.args.respBody,
|
||||
},
|
||||
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
|
||||
testCall(tt.args.ctx, tt.args.timeout, tt.args.body, tt.args.signingKey),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
@ -186,6 +204,29 @@ func Test_CallTarget(t *testing.T) {
|
||||
body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"webhook, signed, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
res{
|
||||
body: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"request response, error",
|
||||
args{
|
||||
@ -228,6 +269,29 @@ func Test_CallTarget(t *testing.T) {
|
||||
body: []byte("{\"request\":\"content2\"}"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"request response, signed, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
signingKey: "signingkey",
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
SigningKey: "signingkey",
|
||||
},
|
||||
},
|
||||
res{
|
||||
body: []byte("{\"request\":\"content2\"}"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -392,6 +456,7 @@ type mockTarget struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
SigningKey string
|
||||
}
|
||||
|
||||
func (e *mockTarget) GetTargetID() string {
|
||||
@ -409,6 +474,9 @@ func (e *mockTarget) GetTargetType() domain.TargetType {
|
||||
func (e *mockTarget) GetTimeout() time.Duration {
|
||||
return e.Timeout
|
||||
}
|
||||
func (e *mockTarget) GetSigningKey() string {
|
||||
return e.SigningKey
|
||||
}
|
||||
|
||||
type callTestServer struct {
|
||||
method string
|
||||
@ -416,6 +484,7 @@ type callTestServer struct {
|
||||
timeout time.Duration
|
||||
statusCode int
|
||||
respondBody []byte
|
||||
signingKey string
|
||||
}
|
||||
|
||||
func testServers(
|
||||
@ -447,7 +516,7 @@ func listen(
|
||||
c *callTestServer,
|
||||
) (url string, close func()) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
checkRequest(t, r, c.method, c.expectBody)
|
||||
checkRequest(t, r, c.method, c.expectBody, c.signingKey)
|
||||
|
||||
if c.statusCode != http.StatusOK {
|
||||
http.Error(w, "error", c.statusCode)
|
||||
@ -466,16 +535,19 @@ func listen(
|
||||
return server.URL, server.Close
|
||||
}
|
||||
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte, signingKey string) {
|
||||
sentBody, err := io.ReadAll(sent.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, sentBody)
|
||||
require.Equal(t, method, sent.Method)
|
||||
if signingKey != "" {
|
||||
require.NoError(t, actions.ValidatePayload(sentBody, sent.Header.Get(actions.SigningHeader), signingKey))
|
||||
}
|
||||
}
|
||||
|
||||
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
|
||||
func testCall(ctx context.Context, timeout time.Duration, body []byte, signingKey string) func(string) ([]byte, error) {
|
||||
return func(url string) ([]byte, error) {
|
||||
return execution.Call(ctx, url, timeout, body)
|
||||
return execution.Call(ctx, url, timeout, body, signingKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
@ -175,6 +176,11 @@ func (q *Queries) TargetsByExecutionID(ctx context.Context, ids []string) (execu
|
||||
instanceID,
|
||||
database.TextArray[string](ids),
|
||||
)
|
||||
for i := range execution {
|
||||
if err := execution[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return execution, err
|
||||
}
|
||||
|
||||
@ -205,6 +211,11 @@ func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string
|
||||
database.TextArray[string](ids1),
|
||||
database.TextArray[string](ids2),
|
||||
)
|
||||
for i := range execution {
|
||||
if err := execution[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return execution, err
|
||||
}
|
||||
|
||||
@ -352,6 +363,8 @@ type ExecutionTarget struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
signingKey *crypto.CryptoValue
|
||||
SigningKey string
|
||||
}
|
||||
|
||||
func (e *ExecutionTarget) GetExecutionID() string {
|
||||
@ -372,6 +385,21 @@ func (e *ExecutionTarget) GetTargetType() domain.TargetType {
|
||||
func (e *ExecutionTarget) GetTimeout() time.Duration {
|
||||
return e.Timeout
|
||||
}
|
||||
func (e *ExecutionTarget) GetSigningKey() string {
|
||||
return e.SigningKey
|
||||
}
|
||||
|
||||
func (t *ExecutionTarget) decryptSigningKey(alg crypto.EncryptionAlgorithm) error {
|
||||
if t.signingKey == nil {
|
||||
return nil
|
||||
}
|
||||
keyValue, err := crypto.DecryptString(t.signingKey, alg)
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "QUERY-bxevy3YXwy", "Errors.Internal")
|
||||
}
|
||||
t.SigningKey = keyValue
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
||||
targets := make([]*ExecutionTarget, 0)
|
||||
@ -386,6 +414,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
||||
endpoint = &sql.NullString{}
|
||||
timeout = &sql.NullInt64{}
|
||||
interruptOnError = &sql.NullBool{}
|
||||
signingKey = &crypto.CryptoValue{}
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
@ -396,6 +425,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
||||
endpoint,
|
||||
timeout,
|
||||
interruptOnError,
|
||||
signingKey,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@ -409,6 +439,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
||||
target.Endpoint = endpoint.String
|
||||
target.Timeout = time.Duration(timeout.Int64)
|
||||
target.InterruptOnError = interruptOnError.Bool
|
||||
target.signingKey = signingKey
|
||||
|
||||
targets = append(targets, target)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TargetTable = "projections.targets1"
|
||||
TargetTable = "projections.targets2"
|
||||
TargetIDCol = "id"
|
||||
TargetCreationDateCol = "creation_date"
|
||||
TargetChangeDateCol = "change_date"
|
||||
@ -23,6 +23,7 @@ const (
|
||||
TargetEndpointCol = "endpoint"
|
||||
TargetTimeoutCol = "timeout"
|
||||
TargetInterruptOnErrorCol = "interrupt_on_error"
|
||||
TargetSigningKey = "signing_key"
|
||||
)
|
||||
|
||||
type targetProjection struct{}
|
||||
@ -49,6 +50,7 @@ func (*targetProjection) Init() *old_handler.Check {
|
||||
handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
|
||||
handler.NewColumn(TargetSigningKey, handler.ColumnTypeJSONB, handler.Nullable()),
|
||||
},
|
||||
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
|
||||
),
|
||||
@ -105,6 +107,7 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S
|
||||
handler.NewCol(TargetTargetType, e.TargetType),
|
||||
handler.NewCol(TargetTimeoutCol, e.Timeout),
|
||||
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
|
||||
handler.NewCol(TargetSigningKey, e.SigningKey),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
@ -134,6 +137,9 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler
|
||||
if e.InterruptOnError != nil {
|
||||
values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError))
|
||||
}
|
||||
if e.SigningKey != nil {
|
||||
values = append(values, handler.NewCol(TargetSigningKey, e.SigningKey))
|
||||
}
|
||||
return handler.NewUpdateStatement(
|
||||
e,
|
||||
values,
|
||||
|
@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
target.AddedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
|
||||
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }}`),
|
||||
),
|
||||
eventstore.GenericEventMapper[target.AddedEvent],
|
||||
),
|
||||
@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.targets1 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||
expectedStmt: "INSERT INTO projections.targets2 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error, signing_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"ro-id",
|
||||
@ -54,6 +54,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
domain.TargetTypeWebhook,
|
||||
3 * time.Second,
|
||||
true,
|
||||
anyArg{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -67,7 +68,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
testEvent(
|
||||
target.ChangedEventType,
|
||||
target.AggregateType,
|
||||
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
|
||||
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }}`),
|
||||
),
|
||||
eventstore.GenericEventMapper[target.ChangedEvent],
|
||||
),
|
||||
@ -79,7 +80,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.targets1 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (instance_id = $9) AND (id = $10)",
|
||||
expectedStmt: "UPDATE projections.targets2 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error, signing_key) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
uint64(15),
|
||||
@ -89,6 +90,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
"https://example.com",
|
||||
3 * time.Second,
|
||||
true,
|
||||
anyArg{},
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
},
|
||||
@ -116,7 +118,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1) AND (id = $2)",
|
||||
expectedStmt: "DELETE FROM projections.targets2 WHERE (instance_id = $1) AND (id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"instance-id",
|
||||
"agg-id",
|
||||
@ -145,7 +147,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1)",
|
||||
expectedStmt: "DELETE FROM projections.targets2 WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
|
@ -29,10 +29,11 @@ type Queries struct {
|
||||
client *database.DB
|
||||
caches *Caches
|
||||
|
||||
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
||||
checkPermission domain.PermissionCheck
|
||||
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||
targetEncryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
||||
checkPermission domain.PermissionCheck
|
||||
|
||||
DefaultLanguage language.Tag
|
||||
mutex sync.Mutex
|
||||
@ -52,7 +53,7 @@ func StartQueries(
|
||||
cacheConnectors connector.Connectors,
|
||||
projections projection.Config,
|
||||
defaults sd.SystemDefaults,
|
||||
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm, targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||
zitadelRoles []authz.RoleMapping,
|
||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||
permissionCheck func(q *Queries) domain.PermissionCheck,
|
||||
@ -70,6 +71,7 @@ func StartQueries(
|
||||
zitadelRoles: zitadelRoles,
|
||||
keyEncryptionAlgorithm: keyEncryptionAlgorithm,
|
||||
idpConfigEncryption: idpConfigEncryption,
|
||||
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
|
||||
sessionTokenVerifier: sessionTokenVerifier,
|
||||
multifactors: domain.MultifactorConfigs{
|
||||
OTP: domain.OTPConfig{
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@ -59,6 +60,10 @@ var (
|
||||
name: projection.TargetInterruptOnErrorCol,
|
||||
table: targetTable,
|
||||
}
|
||||
TargetColumnSigningKey = Column{
|
||||
name: projection.TargetSigningKey,
|
||||
table: targetTable,
|
||||
}
|
||||
)
|
||||
|
||||
type Targets struct {
|
||||
@ -78,6 +83,20 @@ type Target struct {
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
signingKey *crypto.CryptoValue
|
||||
SigningKey string
|
||||
}
|
||||
|
||||
func (t *Target) decryptSigningKey(alg crypto.EncryptionAlgorithm) error {
|
||||
if t.signingKey == nil {
|
||||
return nil
|
||||
}
|
||||
keyValue, err := crypto.DecryptString(t.signingKey, alg)
|
||||
if err != nil {
|
||||
return zerrors.ThrowInternal(err, "QUERY-bxevy3YXwy", "Errors.Internal")
|
||||
}
|
||||
t.SigningKey = keyValue
|
||||
return nil
|
||||
}
|
||||
|
||||
type TargetSearchQueries struct {
|
||||
@ -93,21 +112,37 @@ func (q *TargetSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
||||
return query
|
||||
}
|
||||
|
||||
func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQueries) (targets *Targets, err error) {
|
||||
func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQueries) (*Targets, error) {
|
||||
eq := sq.Eq{
|
||||
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}
|
||||
query, scan := prepareTargetsQuery(ctx, q.client)
|
||||
return genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan)
|
||||
targets, err := genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range targets.Targets {
|
||||
if err := targets.Targets[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func (q *Queries) GetTargetByID(ctx context.Context, id string) (target *Target, err error) {
|
||||
func (q *Queries) GetTargetByID(ctx context.Context, id string) (*Target, error) {
|
||||
eq := sq.Eq{
|
||||
TargetColumnID.identifier(): id,
|
||||
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}
|
||||
query, scan := prepareTargetQuery(ctx, q.client)
|
||||
return genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan)
|
||||
target, err := genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := target.decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func NewTargetNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
|
||||
@ -129,6 +164,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu
|
||||
TargetColumnTimeout.identifier(),
|
||||
TargetColumnURL.identifier(),
|
||||
TargetColumnInterruptOnError.identifier(),
|
||||
TargetColumnSigningKey.identifier(),
|
||||
countColumn.identifier(),
|
||||
).From(targetTable.identifier()).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
@ -147,6 +183,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu
|
||||
&target.Timeout,
|
||||
&target.Endpoint,
|
||||
&target.InterruptOnError,
|
||||
&target.signingKey,
|
||||
&count,
|
||||
)
|
||||
if err != nil {
|
||||
@ -179,6 +216,7 @@ func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fun
|
||||
TargetColumnTimeout.identifier(),
|
||||
TargetColumnURL.identifier(),
|
||||
TargetColumnInterruptOnError.identifier(),
|
||||
TargetColumnSigningKey.identifier(),
|
||||
).From(targetTable.identifier()).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*Target, error) {
|
||||
@ -193,6 +231,7 @@ func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fun
|
||||
&target.Timeout,
|
||||
&target.Endpoint,
|
||||
&target.InterruptOnError,
|
||||
&target.signingKey,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
@ -9,22 +9,24 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
prepareTargetsStmt = `SELECT projections.targets1.id,` +
|
||||
` projections.targets1.creation_date,` +
|
||||
` projections.targets1.change_date,` +
|
||||
` projections.targets1.resource_owner,` +
|
||||
` projections.targets1.name,` +
|
||||
` projections.targets1.target_type,` +
|
||||
` projections.targets1.timeout,` +
|
||||
` projections.targets1.endpoint,` +
|
||||
` projections.targets1.interrupt_on_error,` +
|
||||
prepareTargetsStmt = `SELECT projections.targets2.id,` +
|
||||
` projections.targets2.creation_date,` +
|
||||
` projections.targets2.change_date,` +
|
||||
` projections.targets2.resource_owner,` +
|
||||
` projections.targets2.name,` +
|
||||
` projections.targets2.target_type,` +
|
||||
` projections.targets2.timeout,` +
|
||||
` projections.targets2.endpoint,` +
|
||||
` projections.targets2.interrupt_on_error,` +
|
||||
` projections.targets2.signing_key,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.targets1`
|
||||
` FROM projections.targets2`
|
||||
prepareTargetsCols = []string{
|
||||
"id",
|
||||
"creation_date",
|
||||
@ -35,19 +37,21 @@ var (
|
||||
"timeout",
|
||||
"endpoint",
|
||||
"interrupt_on_error",
|
||||
"signing_key",
|
||||
"count",
|
||||
}
|
||||
|
||||
prepareTargetStmt = `SELECT projections.targets1.id,` +
|
||||
` projections.targets1.creation_date,` +
|
||||
` projections.targets1.change_date,` +
|
||||
` projections.targets1.resource_owner,` +
|
||||
` projections.targets1.name,` +
|
||||
` projections.targets1.target_type,` +
|
||||
` projections.targets1.timeout,` +
|
||||
` projections.targets1.endpoint,` +
|
||||
` projections.targets1.interrupt_on_error` +
|
||||
` FROM projections.targets1`
|
||||
prepareTargetStmt = `SELECT projections.targets2.id,` +
|
||||
` projections.targets2.creation_date,` +
|
||||
` projections.targets2.change_date,` +
|
||||
` projections.targets2.resource_owner,` +
|
||||
` projections.targets2.name,` +
|
||||
` projections.targets2.target_type,` +
|
||||
` projections.targets2.timeout,` +
|
||||
` projections.targets2.endpoint,` +
|
||||
` projections.targets2.interrupt_on_error,` +
|
||||
` projections.targets2.signing_key` +
|
||||
` FROM projections.targets2`
|
||||
prepareTargetCols = []string{
|
||||
"id",
|
||||
"creation_date",
|
||||
@ -58,6 +62,7 @@ var (
|
||||
"timeout",
|
||||
"endpoint",
|
||||
"interrupt_on_error",
|
||||
"signing_key",
|
||||
}
|
||||
)
|
||||
|
||||
@ -102,6 +107,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
1 * time.Second,
|
||||
"https://example.com",
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -123,6 +134,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
Timeout: 1 * time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
InterruptOnError: true,
|
||||
signingKey: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -145,6 +162,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
1 * time.Second,
|
||||
"https://example.com",
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"id-2",
|
||||
@ -156,6 +179,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
1 * time.Second,
|
||||
"https://example.com",
|
||||
false,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"id-3",
|
||||
@ -167,6 +196,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
1 * time.Second,
|
||||
"https://example.com",
|
||||
false,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -188,6 +223,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
Timeout: 1 * time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
InterruptOnError: true,
|
||||
signingKey: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
@ -201,6 +242,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
Timeout: 1 * time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
InterruptOnError: false,
|
||||
signingKey: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectDetails: domain.ObjectDetails{
|
||||
@ -214,6 +261,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
Timeout: 1 * time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
InterruptOnError: false,
|
||||
signingKey: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -270,6 +323,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
1 * time.Second,
|
||||
"https://example.com",
|
||||
true,
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
@ -285,6 +344,12 @@ func Test_TargetPrepares(t *testing.T) {
|
||||
Timeout: 1 * time.Second,
|
||||
Endpoint: "https://example.com",
|
||||
InterruptOnError: true,
|
||||
signingKey: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "alg",
|
||||
KeyID: "encKey",
|
||||
Crypted: []byte("crypted"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -31,9 +31,9 @@ WITH RECURSIVE
|
||||
ON e.instance_id = p.instance_id
|
||||
AND e.include IS NOT NULL
|
||||
AND e.include = p.execution_id)
|
||||
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
|
||||
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error, t.signing_key
|
||||
FROM dissolved_execution_targets e
|
||||
JOIN projections.targets1 t
|
||||
JOIN projections.targets2 t
|
||||
ON e.instance_id = t.instance_id
|
||||
AND e.target_id = t.id
|
||||
WHERE "include" = ''
|
||||
|
@ -38,9 +38,9 @@ WITH RECURSIVE
|
||||
ON e.instance_id = p.instance_id
|
||||
AND e.include IS NOT NULL
|
||||
AND e.include = p.execution_id)
|
||||
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
|
||||
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error, t.signing_key
|
||||
FROM dissolved_execution_targets e
|
||||
JOIN projections.targets1 t
|
||||
JOIN projections.targets2 t
|
||||
ON e.instance_id = t.instance_id
|
||||
AND e.target_id = t.id
|
||||
WHERE "include" = ''
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
@ -18,11 +19,12 @@ const (
|
||||
type AddedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Name string `json:"name"`
|
||||
TargetType domain.TargetType `json:"targetType"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
InterruptOnError bool `json:"interruptOnError"`
|
||||
Name string `json:"name"`
|
||||
TargetType domain.TargetType `json:"targetType"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
InterruptOnError bool `json:"interruptOnError"`
|
||||
SigningKey *crypto.CryptoValue `json:"signingKey"`
|
||||
}
|
||||
|
||||
func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||
@ -45,22 +47,24 @@ func NewAddedEvent(
|
||||
endpoint string,
|
||||
timeout time.Duration,
|
||||
interruptOnError bool,
|
||||
signingKey *crypto.CryptoValue,
|
||||
) *AddedEvent {
|
||||
return &AddedEvent{
|
||||
*eventstore.NewBaseEventForPush(
|
||||
ctx, aggregate, AddedEventType,
|
||||
),
|
||||
name, targetType, endpoint, timeout, interruptOnError}
|
||||
name, targetType, endpoint, timeout, interruptOnError, signingKey}
|
||||
}
|
||||
|
||||
type ChangedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Name *string `json:"name,omitempty"`
|
||||
TargetType *domain.TargetType `json:"targetType,omitempty"`
|
||||
Endpoint *string `json:"endpoint,omitempty"`
|
||||
Timeout *time.Duration `json:"timeout,omitempty"`
|
||||
InterruptOnError *bool `json:"interruptOnError,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
TargetType *domain.TargetType `json:"targetType,omitempty"`
|
||||
Endpoint *string `json:"endpoint,omitempty"`
|
||||
Timeout *time.Duration `json:"timeout,omitempty"`
|
||||
InterruptOnError *bool `json:"interruptOnError,omitempty"`
|
||||
SigningKey *crypto.CryptoValue `json:"signingKey,omitempty"`
|
||||
|
||||
oldName string
|
||||
}
|
||||
@ -134,6 +138,12 @@ func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
func ChangeSigningKey(signingKey *crypto.CryptoValue) func(event *ChangedEvent) {
|
||||
return func(e *ChangedEvent) {
|
||||
e.SigningKey = signingKey
|
||||
}
|
||||
}
|
||||
|
||||
type RemovedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
|
115
pkg/actions/signing.go
Normal file
115
pkg/actions/signing.go
Normal file
@ -0,0 +1,115 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoValidSignature = errors.New("no valid signature")
|
||||
ErrInvalidHeader = errors.New("webhook has invalid Zitadel-Signature header")
|
||||
ErrNotSigned = errors.New("webhook has no Zitadel-Signature header")
|
||||
ErrTooOld = errors.New("timestamp wasn't within tolerance")
|
||||
)
|
||||
|
||||
const (
|
||||
SigningHeader = "ZITADEL-Signature"
|
||||
signingTimestamp = "t"
|
||||
signingVersion string = "v1"
|
||||
DefaultTolerance = 300 * time.Second
|
||||
partSeparator = ","
|
||||
)
|
||||
|
||||
func ComputeSignatureHeader(t time.Time, payload []byte, signingKey ...string) string {
|
||||
parts := []string{
|
||||
fmt.Sprintf("%s=%d", signingTimestamp, t.Unix()),
|
||||
}
|
||||
for _, k := range signingKey {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", signingVersion, hex.EncodeToString(computeSignature(t, payload, k))))
|
||||
}
|
||||
return strings.Join(parts, partSeparator)
|
||||
}
|
||||
|
||||
func computeSignature(t time.Time, payload []byte, signingKey string) []byte {
|
||||
mac := hmac.New(sha256.New, []byte(signingKey))
|
||||
mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
|
||||
mac.Write([]byte("."))
|
||||
mac.Write(payload)
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func ValidatePayload(payload []byte, header string, signingKey string) error {
|
||||
return ValidatePayloadWithTolerance(payload, header, signingKey, DefaultTolerance)
|
||||
}
|
||||
|
||||
func ValidatePayloadWithTolerance(payload []byte, header string, signingKey string, tolerance time.Duration) error {
|
||||
return validatePayload(payload, header, signingKey, tolerance, true)
|
||||
}
|
||||
|
||||
func validatePayload(payload []byte, sigHeader string, signingKey string, tolerance time.Duration, enforceTolerance bool) error {
|
||||
header, err := parseSignatureHeader(sigHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedSignature := computeSignature(header.timestamp, payload, signingKey)
|
||||
expiredTimestamp := time.Since(header.timestamp) > tolerance
|
||||
if enforceTolerance && expiredTimestamp {
|
||||
return ErrTooOld
|
||||
}
|
||||
|
||||
for _, sig := range header.signatures {
|
||||
if hmac.Equal(expectedSignature, sig) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrNoValidSignature
|
||||
}
|
||||
|
||||
type signedHeader struct {
|
||||
timestamp time.Time
|
||||
signatures [][]byte
|
||||
}
|
||||
|
||||
func parseSignatureHeader(header string) (*signedHeader, error) {
|
||||
sh := &signedHeader{}
|
||||
if header == "" {
|
||||
return sh, ErrNotSigned
|
||||
}
|
||||
|
||||
pairs := strings.Split(header, ",")
|
||||
for _, pair := range pairs {
|
||||
parts := strings.Split(pair, "=")
|
||||
if len(parts) != 2 {
|
||||
return sh, ErrInvalidHeader
|
||||
}
|
||||
switch parts[0] {
|
||||
case signingTimestamp:
|
||||
timestamp, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return sh, ErrInvalidHeader
|
||||
}
|
||||
sh.timestamp = time.Unix(timestamp, 0)
|
||||
|
||||
case signingVersion:
|
||||
sig, err := hex.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sh.signatures = append(sh.signatures, sig)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(sh.signatures) == 0 {
|
||||
return sh, ErrNoValidSignature
|
||||
}
|
||||
return sh, nil
|
||||
}
|
@ -408,6 +408,12 @@ message CreateTargetRequest {
|
||||
|
||||
message CreateTargetResponse {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
// Key used to sign and check payload sent to the target.
|
||||
string signing_key = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"98KmsU67\""
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message PatchTargetRequest {
|
||||
@ -433,6 +439,12 @@ message PatchTargetRequest {
|
||||
|
||||
message PatchTargetResponse {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
// Key used to sign and check payload sent to the target.
|
||||
optional string signing_key = 2 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"98KmsU67\""
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message DeleteTargetRequest {
|
||||
|
@ -9,6 +9,7 @@ import "google/protobuf/struct.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
import "zitadel/resources/object/v3alpha/object.proto";
|
||||
|
||||
@ -51,6 +52,11 @@ message Target {
|
||||
message GetTarget {
|
||||
zitadel.resources.object.v3alpha.Details details = 1;
|
||||
Target config = 2;
|
||||
string signing_key = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"98KmsU67\""
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message PatchTarget {
|
||||
@ -84,6 +90,21 @@ message PatchTarget {
|
||||
max_length: 1000
|
||||
}
|
||||
];
|
||||
// Regenerate the key used for signing and checking the payload sent to the target.
|
||||
// Set the graceful period for the existing key. During that time, the previous
|
||||
// signing key and the new one will be used to sign the request to allow you a smooth
|
||||
// transition onf your API.
|
||||
//
|
||||
// Note that we currently only allow an immediate rotation ("0s") and will support
|
||||
// longer expirations in the future.
|
||||
optional google.protobuf.Duration expiration_signing_key = 7 [
|
||||
(validate.rules).duration = {const: {seconds: 0, nanos: 0}},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
example: "\"0s\""
|
||||
minimum: 0
|
||||
maximum: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user