mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +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:
|
User:
|
||||||
EncryptionKeyID: "userKey" # ZITADEL_ENCRYPTIONKEYS_USER_ENCRYPTIONKEYID
|
EncryptionKeyID: "userKey" # ZITADEL_ENCRYPTIONKEYS_USER_ENCRYPTIONKEYID
|
||||||
DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_USER_DECRYPTIONKEYIDS (comma separated list)
|
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
|
CSRFCookieKeyID: "csrfCookieKey" # ZITADEL_ENCRYPTIONKEYS_CSRFCOOKIEKEYID
|
||||||
UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID
|
UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID
|
||||||
|
|
||||||
@ -910,6 +913,12 @@ DefaultInstance:
|
|||||||
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEUPPERLETTERS
|
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEUPPERLETTERS
|
||||||
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEDIGITS
|
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEDIGITS
|
||||||
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDESYMBOLS
|
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:
|
PasswordComplexityPolicy:
|
||||||
MinLength: 8 # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_MINLENGTH
|
MinLength: 8 # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_MINLENGTH
|
||||||
HasLowercase: true # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_HASLOWERCASE
|
HasLowercase: true # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_HASLOWERCASE
|
||||||
|
@ -17,6 +17,7 @@ var (
|
|||||||
"smsKey",
|
"smsKey",
|
||||||
"smtpKey",
|
"smtpKey",
|
||||||
"userKey",
|
"userKey",
|
||||||
|
"targetKey",
|
||||||
"csrfCookieKey",
|
"csrfCookieKey",
|
||||||
"userAgentCookieKey",
|
"userAgentCookieKey",
|
||||||
}
|
}
|
||||||
@ -31,6 +32,7 @@ type EncryptionKeyConfig struct {
|
|||||||
SMS *crypto.KeyConfig
|
SMS *crypto.KeyConfig
|
||||||
SMTP *crypto.KeyConfig
|
SMTP *crypto.KeyConfig
|
||||||
User *crypto.KeyConfig
|
User *crypto.KeyConfig
|
||||||
|
Target *crypto.KeyConfig
|
||||||
CSRFCookieKeyID string
|
CSRFCookieKeyID string
|
||||||
UserAgentCookieKeyID string
|
UserAgentCookieKeyID string
|
||||||
}
|
}
|
||||||
@ -44,6 +46,7 @@ type EncryptionKeys struct {
|
|||||||
SMS crypto.EncryptionAlgorithm
|
SMS crypto.EncryptionAlgorithm
|
||||||
SMTP crypto.EncryptionAlgorithm
|
SMTP crypto.EncryptionAlgorithm
|
||||||
User crypto.EncryptionAlgorithm
|
User crypto.EncryptionAlgorithm
|
||||||
|
Target crypto.EncryptionAlgorithm
|
||||||
CSRFCookieKey []byte
|
CSRFCookieKey []byte
|
||||||
UserAgentCookieKey []byte
|
UserAgentCookieKey []byte
|
||||||
OIDCKey []byte
|
OIDCKey []byte
|
||||||
@ -91,6 +94,10 @@ func EnsureEncryptionKeys(ctx context.Context, keyConfig *EncryptionKeyConfig, k
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
keys.Target, err = crypto.NewAESCrypto(keyConfig.Target, keyStorage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
key, err = crypto.LoadKey(keyConfig.CSRFCookieKeyID, keyStorage)
|
key, err = crypto.LoadKey(keyConfig.CSRFCookieKeyID, keyStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -145,6 +145,7 @@ func projections(
|
|||||||
keys.OTP,
|
keys.OTP,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
|
keys.Target,
|
||||||
config.InternalAuthZ.RolePermissionMappings,
|
config.InternalAuthZ.RolePermissionMappings,
|
||||||
sessionTokenVerifier,
|
sessionTokenVerifier,
|
||||||
func(q *query.Queries) domain.PermissionCheck {
|
func(q *query.Queries) domain.PermissionCheck {
|
||||||
@ -183,6 +184,7 @@ func projections(
|
|||||||
keys.DomainVerification,
|
keys.DomainVerification,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
|
keys.Target,
|
||||||
&http.Client{},
|
&http.Client{},
|
||||||
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||||
return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
|
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,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
@ -53,6 +53,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
@ -366,6 +366,7 @@ func initProjections(
|
|||||||
keys.OTP,
|
keys.OTP,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
|
keys.Target,
|
||||||
config.InternalAuthZ.RolePermissionMappings,
|
config.InternalAuthZ.RolePermissionMappings,
|
||||||
sessionTokenVerifier,
|
sessionTokenVerifier,
|
||||||
func(q *query.Queries) domain.PermissionCheck {
|
func(q *query.Queries) domain.PermissionCheck {
|
||||||
@ -422,6 +423,7 @@ func initProjections(
|
|||||||
keys.DomainVerification,
|
keys.DomainVerification,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
|
keys.Target,
|
||||||
&http.Client{},
|
&http.Client{},
|
||||||
permissionCheck,
|
permissionCheck,
|
||||||
sessionTokenVerifier,
|
sessionTokenVerifier,
|
||||||
|
@ -196,6 +196,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
|||||||
keys.OTP,
|
keys.OTP,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
|
keys.Target,
|
||||||
config.InternalAuthZ.RolePermissionMappings,
|
config.InternalAuthZ.RolePermissionMappings,
|
||||||
sessionTokenVerifier,
|
sessionTokenVerifier,
|
||||||
func(q *query.Queries) domain.PermissionCheck {
|
func(q *query.Queries) domain.PermissionCheck {
|
||||||
@ -245,6 +246,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
|||||||
keys.DomainVerification,
|
keys.DomainVerification,
|
||||||
keys.OIDC,
|
keys.OIDC,
|
||||||
keys.SAML,
|
keys.SAML,
|
||||||
|
keys.Target,
|
||||||
&http.Client{},
|
&http.Client{},
|
||||||
permissionCheck,
|
permissionCheck,
|
||||||
sessionTokenVerifier,
|
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.
|
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
|
## 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:
|
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)
|
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
|
## Execution
|
||||||
|
|
||||||
ZITADEL decides on specific conditions if one or more Targets have to be called.
|
ZITADEL decides on specific conditions if one or more Targets have to be called.
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"generate:grpc": "buf generate ../proto",
|
"generate:grpc": "buf generate ../proto",
|
||||||
"generate:apidocs": "docusaurus gen-api-docs all",
|
"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: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"
|
"generate:clean-all": "docusaurus clean-api-docs all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -62,6 +62,7 @@ func TestServer_GetTarget(t *testing.T) {
|
|||||||
request.Id = resp.GetDetails().GetId()
|
request.Id = resp.GetDetails().GetId()
|
||||||
response.Target.Config.Name = name
|
response.Target.Config.Name = name
|
||||||
response.Target.Details = resp.GetDetails()
|
response.Target.Details = resp.GetDetails()
|
||||||
|
response.Target.SigningKey = resp.GetSigningKey()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
req: &action.GetTargetRequest{},
|
req: &action.GetTargetRequest{},
|
||||||
@ -92,6 +93,7 @@ func TestServer_GetTarget(t *testing.T) {
|
|||||||
request.Id = resp.GetDetails().GetId()
|
request.Id = resp.GetDetails().GetId()
|
||||||
response.Target.Config.Name = name
|
response.Target.Config.Name = name
|
||||||
response.Target.Details = resp.GetDetails()
|
response.Target.Details = resp.GetDetails()
|
||||||
|
response.Target.SigningKey = resp.GetSigningKey()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
req: &action.GetTargetRequest{},
|
req: &action.GetTargetRequest{},
|
||||||
@ -122,6 +124,7 @@ func TestServer_GetTarget(t *testing.T) {
|
|||||||
request.Id = resp.GetDetails().GetId()
|
request.Id = resp.GetDetails().GetId()
|
||||||
response.Target.Config.Name = name
|
response.Target.Config.Name = name
|
||||||
response.Target.Details = resp.GetDetails()
|
response.Target.Details = resp.GetDetails()
|
||||||
|
response.Target.SigningKey = resp.GetSigningKey()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
req: &action.GetTargetRequest{},
|
req: &action.GetTargetRequest{},
|
||||||
@ -154,6 +157,7 @@ func TestServer_GetTarget(t *testing.T) {
|
|||||||
request.Id = resp.GetDetails().GetId()
|
request.Id = resp.GetDetails().GetId()
|
||||||
response.Target.Config.Name = name
|
response.Target.Config.Name = name
|
||||||
response.Target.Details = resp.GetDetails()
|
response.Target.Details = resp.GetDetails()
|
||||||
|
response.Target.SigningKey = resp.GetSigningKey()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
req: &action.GetTargetRequest{},
|
req: &action.GetTargetRequest{},
|
||||||
@ -186,6 +190,7 @@ func TestServer_GetTarget(t *testing.T) {
|
|||||||
request.Id = resp.GetDetails().GetId()
|
request.Id = resp.GetDetails().GetId()
|
||||||
response.Target.Config.Name = name
|
response.Target.Config.Name = name
|
||||||
response.Target.Details = resp.GetDetails()
|
response.Target.Details = resp.GetDetails()
|
||||||
|
response.Target.SigningKey = resp.GetSigningKey()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
req: &action.GetTargetRequest{},
|
req: &action.GetTargetRequest{},
|
||||||
@ -230,6 +235,7 @@ func TestServer_GetTarget(t *testing.T) {
|
|||||||
gotTarget := got.GetTarget()
|
gotTarget := got.GetTarget()
|
||||||
integration.AssertResourceDetails(ttt, wantTarget.GetDetails(), gotTarget.GetDetails())
|
integration.AssertResourceDetails(ttt, wantTarget.GetDetails(), gotTarget.GetDetails())
|
||||||
assert.EqualExportedValues(ttt, wantTarget.GetConfig(), gotTarget.GetConfig())
|
assert.EqualExportedValues(ttt, wantTarget.GetConfig(), gotTarget.GetConfig())
|
||||||
|
assert.Equal(ttt, wantTarget.GetSigningKey(), gotTarget.GetSigningKey())
|
||||||
}, retryDuration, tick, "timeout waiting for expected target result")
|
}, retryDuration, tick, "timeout waiting for expected target result")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -492,6 +498,7 @@ func TestServer_ListTargets(t *testing.T) {
|
|||||||
for i := range tt.want.Result {
|
for i := range tt.want.Result {
|
||||||
integration.AssertResourceDetails(ttt, tt.want.Result[i].GetDetails(), got.Result[i].GetDetails())
|
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.EqualExportedValues(ttt, tt.want.Result[i].GetConfig(), got.Result[i].GetConfig())
|
||||||
|
assert.NotEmpty(ttt, got.Result[i].GetSigningKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
integration.AssertResourceListDetails(ttt, tt.want, got)
|
integration.AssertResourceListDetails(ttt, tt.want, got)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v6"
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"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) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
|
got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
require.Error(t, err)
|
assert.Error(t, err)
|
||||||
return
|
} else {
|
||||||
}
|
assert.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||||
|
assert.NotEmpty(t, got.GetSigningKey())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,11 +219,15 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
req *action.PatchTargetRequest
|
req *action.PatchTargetRequest
|
||||||
}
|
}
|
||||||
|
type want struct {
|
||||||
|
details *resource_object.Details
|
||||||
|
signingKey bool
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
prepare func(request *action.PatchTargetRequest) error
|
prepare func(request *action.PatchTargetRequest) error
|
||||||
args args
|
args args
|
||||||
want *resource_object.Details
|
want want
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -272,7 +278,8 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &resource_object.Details{
|
want: want{
|
||||||
|
details: &resource_object.Details{
|
||||||
Changed: timestamppb.Now(),
|
Changed: timestamppb.Now(),
|
||||||
Owner: &object.Owner{
|
Owner: &object.Owner{
|
||||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||||
@ -280,6 +287,33 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
name: "change type, ok",
|
||||||
prepare: func(request *action.PatchTargetRequest) error {
|
prepare: func(request *action.PatchTargetRequest) error {
|
||||||
@ -299,7 +333,8 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &resource_object.Details{
|
want: want{
|
||||||
|
details: &resource_object.Details{
|
||||||
Changed: timestamppb.Now(),
|
Changed: timestamppb.Now(),
|
||||||
Owner: &object.Owner{
|
Owner: &object.Owner{
|
||||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||||
@ -307,6 +342,7 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "change url, ok",
|
name: "change url, ok",
|
||||||
prepare: func(request *action.PatchTargetRequest) error {
|
prepare: func(request *action.PatchTargetRequest) error {
|
||||||
@ -322,7 +358,8 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &resource_object.Details{
|
want: want{
|
||||||
|
details: &resource_object.Details{
|
||||||
Changed: timestamppb.Now(),
|
Changed: timestamppb.Now(),
|
||||||
Owner: &object.Owner{
|
Owner: &object.Owner{
|
||||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||||
@ -330,6 +367,7 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "change timeout, ok",
|
name: "change timeout, ok",
|
||||||
prepare: func(request *action.PatchTargetRequest) error {
|
prepare: func(request *action.PatchTargetRequest) error {
|
||||||
@ -345,7 +383,8 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &resource_object.Details{
|
want: want{
|
||||||
|
details: &resource_object.Details{
|
||||||
Changed: timestamppb.Now(),
|
Changed: timestamppb.Now(),
|
||||||
Owner: &object.Owner{
|
Owner: &object.Owner{
|
||||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||||
@ -353,6 +392,7 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "change type async, ok",
|
name: "change type async, ok",
|
||||||
prepare: func(request *action.PatchTargetRequest) error {
|
prepare: func(request *action.PatchTargetRequest) error {
|
||||||
@ -370,7 +410,8 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: &resource_object.Details{
|
want: want{
|
||||||
|
details: &resource_object.Details{
|
||||||
Changed: timestamppb.Now(),
|
Changed: timestamppb.Now(),
|
||||||
Owner: &object.Owner{
|
Owner: &object.Owner{
|
||||||
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
|
||||||
@ -378,6 +419,7 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -387,11 +429,14 @@ func TestServer_PatchTarget(t *testing.T) {
|
|||||||
instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
|
instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
|
||||||
got, err := 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 {
|
if tt.wantErr {
|
||||||
require.Error(t, err)
|
assert.Error(t, err)
|
||||||
return
|
} 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) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
|
got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
require.Error(t, err)
|
assert.Error(t, err)
|
||||||
return
|
return
|
||||||
}
|
} else {
|
||||||
require.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
integration.AssertResourceDetails(t, tt.want, got.Details)
|
integration.AssertResourceDetails(t, tt.want, got.Details)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ func targetToPb(t *query.Target) *action.GetTarget {
|
|||||||
Timeout: durationpb.New(t.Timeout),
|
Timeout: durationpb.New(t.Timeout),
|
||||||
Endpoint: t.Endpoint,
|
Endpoint: t.Endpoint,
|
||||||
},
|
},
|
||||||
|
SigningKey: t.SigningKey,
|
||||||
}
|
}
|
||||||
switch t.TargetType {
|
switch t.TargetType {
|
||||||
case domain.TargetTypeWebhook:
|
case domain.TargetTypeWebhook:
|
||||||
|
@ -26,6 +26,7 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque
|
|||||||
}
|
}
|
||||||
return &action.CreateTargetResponse{
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,12 +35,14 @@ func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &action.PatchTargetResponse{
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +86,12 @@ func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget {
|
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()
|
reqTarget := req.GetTarget()
|
||||||
if reqTarget == nil {
|
if reqTarget == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -93,6 +102,7 @@ func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget
|
|||||||
},
|
},
|
||||||
Name: reqTarget.Name,
|
Name: reqTarget.Name,
|
||||||
Endpoint: reqTarget.Endpoint,
|
Endpoint: reqTarget.Endpoint,
|
||||||
|
ExpirationSigningKey: expirationSigningKey,
|
||||||
}
|
}
|
||||||
if reqTarget.TargetType != nil {
|
if reqTarget.TargetType != nil {
|
||||||
switch t := reqTarget.GetTargetType().(type) {
|
switch t := reqTarget.GetTargetType().(type) {
|
||||||
|
@ -26,6 +26,7 @@ type mockExecutionTarget struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
InterruptOnError bool
|
InterruptOnError bool
|
||||||
|
SigningKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
|
func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
|
||||||
@ -49,6 +50,9 @@ func (e *mockExecutionTarget) GetTargetID() string {
|
|||||||
func (e *mockExecutionTarget) GetExecutionID() string {
|
func (e *mockExecutionTarget) GetExecutionID() string {
|
||||||
return e.ExecutionID
|
return e.ExecutionID
|
||||||
}
|
}
|
||||||
|
func (e *mockExecutionTarget) GetSigningKey() string {
|
||||||
|
return e.SigningKey
|
||||||
|
}
|
||||||
|
|
||||||
type mockContentRequest struct {
|
type mockContentRequest struct {
|
||||||
Content string
|
Content string
|
||||||
@ -157,6 +161,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -186,6 +191,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -216,6 +222,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -245,6 +252,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -269,6 +277,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -297,6 +306,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeAsync,
|
TargetType: domain.TargetTypeAsync,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -325,6 +335,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetID: "target",
|
TargetID: "target",
|
||||||
TargetType: domain.TargetTypeAsync,
|
TargetType: domain.TargetTypeAsync,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -354,6 +365,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeWebhook,
|
TargetType: domain.TargetTypeWebhook,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -382,6 +394,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeWebhook,
|
TargetType: domain.TargetTypeWebhook,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -411,6 +424,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeWebhook,
|
TargetType: domain.TargetTypeWebhook,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -440,6 +454,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
@ -448,6 +463,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
@ -456,6 +472,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -498,6 +515,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
@ -506,6 +524,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
&mockExecutionTarget{
|
&mockExecutionTarget{
|
||||||
InstanceID: "instance",
|
InstanceID: "instance",
|
||||||
@ -514,6 +533,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Second,
|
Timeout: time.Second,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -692,6 +712,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
@ -721,6 +742,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
|||||||
TargetType: domain.TargetTypeCall,
|
TargetType: domain.TargetTypeCall,
|
||||||
Timeout: time.Minute,
|
Timeout: time.Minute,
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
SigningKey: "signingkey",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targets: []target{
|
targets: []target{
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/execution"
|
"github.com/zitadel/zitadel/internal/repository/execution"
|
||||||
@ -172,6 +173,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
|
|||||||
"https://example.com",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
true,
|
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",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
true,
|
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",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
true,
|
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",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
true,
|
true,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("12345678"),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
expectPushFailed(
|
expectPushFailed(
|
||||||
@ -930,6 +955,12 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
|
|||||||
"https://example.com",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
true,
|
true,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("12345678"),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"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/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
"github.com/zitadel/zitadel/internal/repository/target"
|
"github.com/zitadel/zitadel/internal/repository/target"
|
||||||
@ -19,6 +21,8 @@ type AddTarget struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
InterruptOnError bool
|
InterruptOnError bool
|
||||||
|
|
||||||
|
SigningKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AddTarget) IsValid() error {
|
func (a *AddTarget) IsValid() error {
|
||||||
@ -58,7 +62,11 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
|
|||||||
if wm.State.Exists() {
|
if wm.State.Exists() {
|
||||||
return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists")
|
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(
|
pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent(
|
||||||
ctx,
|
ctx,
|
||||||
TargetAggregateFromWriteModel(&wm.WriteModel),
|
TargetAggregateFromWriteModel(&wm.WriteModel),
|
||||||
@ -67,6 +75,7 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
|
|||||||
add.Endpoint,
|
add.Endpoint,
|
||||||
add.Timeout,
|
add.Timeout,
|
||||||
add.InterruptOnError,
|
add.InterruptOnError,
|
||||||
|
code.Crypted,
|
||||||
))
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -85,6 +94,9 @@ type ChangeTarget struct {
|
|||||||
Endpoint *string
|
Endpoint *string
|
||||||
Timeout *time.Duration
|
Timeout *time.Duration
|
||||||
InterruptOnError *bool
|
InterruptOnError *bool
|
||||||
|
|
||||||
|
ExpirationSigningKey bool
|
||||||
|
SigningKey *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ChangeTarget) IsValid() error {
|
func (a *ChangeTarget) IsValid() error {
|
||||||
@ -120,6 +132,17 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
|
|||||||
if !existing.State.Exists() {
|
if !existing.State.Exists() {
|
||||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound")
|
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(
|
changedEvent := existing.NewChangedEvent(
|
||||||
ctx,
|
ctx,
|
||||||
TargetAggregateFromWriteModel(&existing.WriteModel),
|
TargetAggregateFromWriteModel(&existing.WriteModel),
|
||||||
@ -127,7 +150,9 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
|
|||||||
change.TargetType,
|
change.TargetType,
|
||||||
change.Endpoint,
|
change.Endpoint,
|
||||||
change.Timeout,
|
change.Timeout,
|
||||||
change.InterruptOnError)
|
change.InterruptOnError,
|
||||||
|
changedSigningKey,
|
||||||
|
)
|
||||||
if changedEvent == nil {
|
if changedEvent == nil {
|
||||||
return writeModelToObjectDetails(&existing.WriteModel), nil
|
return writeModelToObjectDetails(&existing.WriteModel), nil
|
||||||
}
|
}
|
||||||
@ -184,3 +209,7 @@ func (c *Commands) getTargetWriteModelByID(ctx context.Context, id string, resou
|
|||||||
}
|
}
|
||||||
return wm, nil
|
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"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/target"
|
"github.com/zitadel/zitadel/internal/repository/target"
|
||||||
@ -18,6 +19,7 @@ type TargetWriteModel struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
InterruptOnError bool
|
InterruptOnError bool
|
||||||
|
SigningKey *crypto.CryptoValue
|
||||||
|
|
||||||
State domain.TargetState
|
State domain.TargetState
|
||||||
}
|
}
|
||||||
@ -41,6 +43,7 @@ func (wm *TargetWriteModel) Reduce() error {
|
|||||||
wm.Endpoint = e.Endpoint
|
wm.Endpoint = e.Endpoint
|
||||||
wm.Timeout = e.Timeout
|
wm.Timeout = e.Timeout
|
||||||
wm.State = domain.TargetActive
|
wm.State = domain.TargetActive
|
||||||
|
wm.SigningKey = e.SigningKey
|
||||||
case *target.ChangedEvent:
|
case *target.ChangedEvent:
|
||||||
if e.Name != nil {
|
if e.Name != nil {
|
||||||
wm.Name = *e.Name
|
wm.Name = *e.Name
|
||||||
@ -57,6 +60,9 @@ func (wm *TargetWriteModel) Reduce() error {
|
|||||||
if e.InterruptOnError != nil {
|
if e.InterruptOnError != nil {
|
||||||
wm.InterruptOnError = *e.InterruptOnError
|
wm.InterruptOnError = *e.InterruptOnError
|
||||||
}
|
}
|
||||||
|
if e.SigningKey != nil {
|
||||||
|
wm.SigningKey = e.SigningKey
|
||||||
|
}
|
||||||
case *target.RemovedEvent:
|
case *target.RemovedEvent:
|
||||||
wm.State = domain.TargetRemoved
|
wm.State = domain.TargetRemoved
|
||||||
}
|
}
|
||||||
@ -84,6 +90,7 @@ func (wm *TargetWriteModel) NewChangedEvent(
|
|||||||
endpoint *string,
|
endpoint *string,
|
||||||
timeout *time.Duration,
|
timeout *time.Duration,
|
||||||
interruptOnError *bool,
|
interruptOnError *bool,
|
||||||
|
signingKey *crypto.CryptoValue,
|
||||||
) *target.ChangedEvent {
|
) *target.ChangedEvent {
|
||||||
changes := make([]target.Changes, 0)
|
changes := make([]target.Changes, 0)
|
||||||
if name != nil && wm.Name != *name {
|
if name != nil && wm.Name != *name {
|
||||||
@ -101,6 +108,10 @@ func (wm *TargetWriteModel) NewChangedEvent(
|
|||||||
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
|
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
|
||||||
changes = append(changes, target.ChangeInterruptOnError(*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 {
|
if len(changes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/repository/target"
|
"github.com/zitadel/zitadel/internal/repository/target"
|
||||||
@ -20,6 +21,12 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
|
|||||||
"https://example.com",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
false,
|
false,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("12345678"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
@ -21,6 +22,8 @@ func TestCommands_AddTarget(t *testing.T) {
|
|||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
idGenerator id.Generator
|
idGenerator id.Generator
|
||||||
|
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||||
|
defaultSecretGenerators *SecretGenerators
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@ -132,10 +135,18 @@ func TestCommands_AddTarget(t *testing.T) {
|
|||||||
"https://example.com",
|
"https://example.com",
|
||||||
time.Second,
|
time.Second,
|
||||||
false,
|
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{
|
args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@ -187,6 +198,8 @@ func TestCommands_AddTarget(t *testing.T) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
idGenerator: mock.ExpectID(t, "id1"),
|
idGenerator: mock.ExpectID(t, "id1"),
|
||||||
|
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
|
||||||
|
defaultSecretGenerators: &SecretGenerators{},
|
||||||
},
|
},
|
||||||
args{
|
args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@ -220,6 +233,8 @@ func TestCommands_AddTarget(t *testing.T) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
idGenerator: mock.ExpectID(t, "id1"),
|
idGenerator: mock.ExpectID(t, "id1"),
|
||||||
|
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
|
||||||
|
defaultSecretGenerators: &SecretGenerators{},
|
||||||
},
|
},
|
||||||
args{
|
args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@ -246,6 +261,8 @@ func TestCommands_AddTarget(t *testing.T) {
|
|||||||
c := &Commands{
|
c := &Commands{
|
||||||
eventstore: tt.fields.eventstore(t),
|
eventstore: tt.fields.eventstore(t),
|
||||||
idGenerator: tt.fields.idGenerator,
|
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)
|
details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
@ -265,6 +282,8 @@ func TestCommands_AddTarget(t *testing.T) {
|
|||||||
func TestCommands_ChangeTarget(t *testing.T) {
|
func TestCommands_ChangeTarget(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
|
||||||
|
defaultSecretGenerators *SecretGenerators
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@ -510,10 +529,18 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
|||||||
target.ChangeTargetType(domain.TargetTypeCall),
|
target.ChangeTargetType(domain.TargetTypeCall),
|
||||||
target.ChangeTimeout(10 * time.Second),
|
target.ChangeTimeout(10 * time.Second),
|
||||||
target.ChangeInterruptOnError(true),
|
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{
|
args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@ -526,6 +553,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
|||||||
TargetType: gu.Ptr(domain.TargetTypeCall),
|
TargetType: gu.Ptr(domain.TargetTypeCall),
|
||||||
Timeout: gu.Ptr(10 * time.Second),
|
Timeout: gu.Ptr(10 * time.Second),
|
||||||
InterruptOnError: gu.Ptr(true),
|
InterruptOnError: gu.Ptr(true),
|
||||||
|
ExpirationSigningKey: true,
|
||||||
},
|
},
|
||||||
resourceOwner: "instance",
|
resourceOwner: "instance",
|
||||||
},
|
},
|
||||||
@ -541,6 +569,8 @@ func TestCommands_ChangeTarget(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
c := &Commands{
|
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)
|
details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
|
@ -54,6 +54,7 @@ type Commands struct {
|
|||||||
smtpEncryption crypto.EncryptionAlgorithm
|
smtpEncryption crypto.EncryptionAlgorithm
|
||||||
smsEncryption crypto.EncryptionAlgorithm
|
smsEncryption crypto.EncryptionAlgorithm
|
||||||
userEncryption crypto.EncryptionAlgorithm
|
userEncryption crypto.EncryptionAlgorithm
|
||||||
|
targetEncryption crypto.EncryptionAlgorithm
|
||||||
userPasswordHasher *crypto.Hasher
|
userPasswordHasher *crypto.Hasher
|
||||||
secretHasher *crypto.Hasher
|
secretHasher *crypto.Hasher
|
||||||
machineKeySize int
|
machineKeySize int
|
||||||
@ -108,7 +109,7 @@ func StartCommands(
|
|||||||
externalDomain string,
|
externalDomain string,
|
||||||
externalSecure bool,
|
externalSecure bool,
|
||||||
externalPort uint16,
|
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,
|
httpClient *http.Client,
|
||||||
permissionCheck domain.PermissionCheck,
|
permissionCheck domain.PermissionCheck,
|
||||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||||
@ -153,6 +154,7 @@ func StartCommands(
|
|||||||
smtpEncryption: smtpEncryption,
|
smtpEncryption: smtpEncryption,
|
||||||
smsEncryption: smsEncryption,
|
smsEncryption: smsEncryption,
|
||||||
userEncryption: userEncryption,
|
userEncryption: userEncryption,
|
||||||
|
targetEncryption: targetEncryption,
|
||||||
userPasswordHasher: userPasswordHasher,
|
userPasswordHasher: userPasswordHasher,
|
||||||
secretHasher: secretHasher,
|
secretHasher: secretHasher,
|
||||||
machineKeySize: int(defaults.SecretGenerators.MachineKeySize),
|
machineKeySize: int(defaults.SecretGenerators.MachineKeySize),
|
||||||
|
@ -157,6 +157,7 @@ type SecretGenerators struct {
|
|||||||
OTPSMS *crypto.GeneratorConfig
|
OTPSMS *crypto.GeneratorConfig
|
||||||
OTPEmail *crypto.GeneratorConfig
|
OTPEmail *crypto.GeneratorConfig
|
||||||
InviteCode *crypto.GeneratorConfig
|
InviteCode *crypto.GeneratorConfig
|
||||||
|
SigningKey *crypto.GeneratorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ZitadelConfig struct {
|
type ZitadelConfig struct {
|
||||||
|
@ -15,6 +15,7 @@ const (
|
|||||||
SecretGeneratorTypeOTPSMS
|
SecretGeneratorTypeOTPSMS
|
||||||
SecretGeneratorTypeOTPEmail
|
SecretGeneratorTypeOTPEmail
|
||||||
SecretGeneratorTypeInviteCode
|
SecretGeneratorTypeInviteCode
|
||||||
|
SecretGeneratorTypeSigningKey
|
||||||
|
|
||||||
secretGeneratorTypeCount
|
secretGeneratorTypeCount
|
||||||
)
|
)
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
"github.com/zitadel/zitadel/pkg/actions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContextInfo interface {
|
type ContextInfo interface {
|
||||||
@ -28,6 +29,7 @@ type Target interface {
|
|||||||
GetEndpoint() string
|
GetEndpoint() string
|
||||||
GetTargetType() domain.TargetType
|
GetTargetType() domain.TargetType
|
||||||
GetTimeout() time.Duration
|
GetTimeout() time.Duration
|
||||||
|
GetSigningKey() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallTargets call a list of targets in order with handling of error and responses
|
// CallTargets call a list of targets in order with handling of error and responses
|
||||||
@ -72,13 +74,13 @@ func CallTarget(
|
|||||||
switch target.GetTargetType() {
|
switch target.GetTargetType() {
|
||||||
// get request, ignore response and return request and error for handling in list of targets
|
// get request, ignore response and return request and error for handling in list of targets
|
||||||
case domain.TargetTypeWebhook:
|
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
|
// get request, return response and error
|
||||||
case domain.TargetTypeCall:
|
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:
|
case domain.TargetTypeAsync:
|
||||||
go func(target Target, info ContextInfoRequest) {
|
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)
|
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
|
||||||
}
|
}
|
||||||
}(target, info)
|
}(target, info)
|
||||||
@ -89,13 +91,13 @@ func CallTarget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// webhook call a webhook, ignore the response but return the errror
|
// webhook call a webhook, ignore the response but return the errror
|
||||||
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error {
|
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) error {
|
||||||
_, err := Call(ctx, url, timeout, body)
|
_, err := Call(ctx, url, timeout, body, signingKey)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call function to do a post HTTP request to a desired url with timeout
|
// 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, cancel := context.WithTimeout(ctx, timeout)
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -108,6 +110,9 @@ func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if signingKey != "" {
|
||||||
|
req.Header.Set(actions.SigningHeader, actions.ComputeSignatureHeader(time.Now(), body, signingKey))
|
||||||
|
}
|
||||||
|
|
||||||
client := http.DefaultClient
|
client := http.DefaultClient
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/execution"
|
"github.com/zitadel/zitadel/internal/execution"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
"github.com/zitadel/zitadel/pkg/actions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Call(t *testing.T) {
|
func Test_Call(t *testing.T) {
|
||||||
@ -29,6 +30,7 @@ func Test_Call(t *testing.T) {
|
|||||||
body []byte
|
body []byte
|
||||||
respBody []byte
|
respBody []byte
|
||||||
statusCode int
|
statusCode int
|
||||||
|
signingKey string
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
body []byte
|
body []byte
|
||||||
@ -84,6 +86,22 @@ func Test_Call(t *testing.T) {
|
|||||||
body: []byte("{\"response\": \"values\"}"),
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -95,7 +113,7 @@ func Test_Call(t *testing.T) {
|
|||||||
statusCode: tt.args.statusCode,
|
statusCode: tt.args.statusCode,
|
||||||
respondBody: tt.args.respBody,
|
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 {
|
if tt.res.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@ -186,6 +204,29 @@ func Test_CallTarget(t *testing.T) {
|
|||||||
body: nil,
|
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",
|
"request response, error",
|
||||||
args{
|
args{
|
||||||
@ -228,6 +269,29 @@ func Test_CallTarget(t *testing.T) {
|
|||||||
body: []byte("{\"request\":\"content2\"}"),
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -392,6 +456,7 @@ type mockTarget struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
InterruptOnError bool
|
InterruptOnError bool
|
||||||
|
SigningKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *mockTarget) GetTargetID() string {
|
func (e *mockTarget) GetTargetID() string {
|
||||||
@ -409,6 +474,9 @@ func (e *mockTarget) GetTargetType() domain.TargetType {
|
|||||||
func (e *mockTarget) GetTimeout() time.Duration {
|
func (e *mockTarget) GetTimeout() time.Duration {
|
||||||
return e.Timeout
|
return e.Timeout
|
||||||
}
|
}
|
||||||
|
func (e *mockTarget) GetSigningKey() string {
|
||||||
|
return e.SigningKey
|
||||||
|
}
|
||||||
|
|
||||||
type callTestServer struct {
|
type callTestServer struct {
|
||||||
method string
|
method string
|
||||||
@ -416,6 +484,7 @@ type callTestServer struct {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
statusCode int
|
statusCode int
|
||||||
respondBody []byte
|
respondBody []byte
|
||||||
|
signingKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func testServers(
|
func testServers(
|
||||||
@ -447,7 +516,7 @@ func listen(
|
|||||||
c *callTestServer,
|
c *callTestServer,
|
||||||
) (url string, close func()) {
|
) (url string, close func()) {
|
||||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if c.statusCode != http.StatusOK {
|
||||||
http.Error(w, "error", c.statusCode)
|
http.Error(w, "error", c.statusCode)
|
||||||
@ -466,16 +535,19 @@ func listen(
|
|||||||
return server.URL, server.Close
|
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)
|
sentBody, err := io.ReadAll(sent.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, expectedBody, sentBody)
|
require.Equal(t, expectedBody, sentBody)
|
||||||
require.Equal(t, method, sent.Method)
|
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 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"
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query/projection"
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
@ -175,6 +176,11 @@ func (q *Queries) TargetsByExecutionID(ctx context.Context, ids []string) (execu
|
|||||||
instanceID,
|
instanceID,
|
||||||
database.TextArray[string](ids),
|
database.TextArray[string](ids),
|
||||||
)
|
)
|
||||||
|
for i := range execution {
|
||||||
|
if err := execution[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return execution, 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](ids1),
|
||||||
database.TextArray[string](ids2),
|
database.TextArray[string](ids2),
|
||||||
)
|
)
|
||||||
|
for i := range execution {
|
||||||
|
if err := execution[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return execution, err
|
return execution, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +363,8 @@ type ExecutionTarget struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
InterruptOnError bool
|
InterruptOnError bool
|
||||||
|
signingKey *crypto.CryptoValue
|
||||||
|
SigningKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExecutionTarget) GetExecutionID() string {
|
func (e *ExecutionTarget) GetExecutionID() string {
|
||||||
@ -372,6 +385,21 @@ func (e *ExecutionTarget) GetTargetType() domain.TargetType {
|
|||||||
func (e *ExecutionTarget) GetTimeout() time.Duration {
|
func (e *ExecutionTarget) GetTimeout() time.Duration {
|
||||||
return e.Timeout
|
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) {
|
func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
||||||
targets := make([]*ExecutionTarget, 0)
|
targets := make([]*ExecutionTarget, 0)
|
||||||
@ -386,6 +414,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
|||||||
endpoint = &sql.NullString{}
|
endpoint = &sql.NullString{}
|
||||||
timeout = &sql.NullInt64{}
|
timeout = &sql.NullInt64{}
|
||||||
interruptOnError = &sql.NullBool{}
|
interruptOnError = &sql.NullBool{}
|
||||||
|
signingKey = &crypto.CryptoValue{}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
@ -396,6 +425,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
|||||||
endpoint,
|
endpoint,
|
||||||
timeout,
|
timeout,
|
||||||
interruptOnError,
|
interruptOnError,
|
||||||
|
signingKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -409,6 +439,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
|
|||||||
target.Endpoint = endpoint.String
|
target.Endpoint = endpoint.String
|
||||||
target.Timeout = time.Duration(timeout.Int64)
|
target.Timeout = time.Duration(timeout.Int64)
|
||||||
target.InterruptOnError = interruptOnError.Bool
|
target.InterruptOnError = interruptOnError.Bool
|
||||||
|
target.signingKey = signingKey
|
||||||
|
|
||||||
targets = append(targets, target)
|
targets = append(targets, target)
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TargetTable = "projections.targets1"
|
TargetTable = "projections.targets2"
|
||||||
TargetIDCol = "id"
|
TargetIDCol = "id"
|
||||||
TargetCreationDateCol = "creation_date"
|
TargetCreationDateCol = "creation_date"
|
||||||
TargetChangeDateCol = "change_date"
|
TargetChangeDateCol = "change_date"
|
||||||
@ -23,6 +23,7 @@ const (
|
|||||||
TargetEndpointCol = "endpoint"
|
TargetEndpointCol = "endpoint"
|
||||||
TargetTimeoutCol = "timeout"
|
TargetTimeoutCol = "timeout"
|
||||||
TargetInterruptOnErrorCol = "interrupt_on_error"
|
TargetInterruptOnErrorCol = "interrupt_on_error"
|
||||||
|
TargetSigningKey = "signing_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
type targetProjection struct{}
|
type targetProjection struct{}
|
||||||
@ -49,6 +50,7 @@ func (*targetProjection) Init() *old_handler.Check {
|
|||||||
handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText),
|
handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText),
|
||||||
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
|
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
|
||||||
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
|
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
|
||||||
|
handler.NewColumn(TargetSigningKey, handler.ColumnTypeJSONB, handler.Nullable()),
|
||||||
},
|
},
|
||||||
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
|
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
|
||||||
),
|
),
|
||||||
@ -105,6 +107,7 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S
|
|||||||
handler.NewCol(TargetTargetType, e.TargetType),
|
handler.NewCol(TargetTargetType, e.TargetType),
|
||||||
handler.NewCol(TargetTimeoutCol, e.Timeout),
|
handler.NewCol(TargetTimeoutCol, e.Timeout),
|
||||||
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
|
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
|
||||||
|
handler.NewCol(TargetSigningKey, e.SigningKey),
|
||||||
},
|
},
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
@ -134,6 +137,9 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler
|
|||||||
if e.InterruptOnError != nil {
|
if e.InterruptOnError != nil {
|
||||||
values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError))
|
values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError))
|
||||||
}
|
}
|
||||||
|
if e.SigningKey != nil {
|
||||||
|
values = append(values, handler.NewCol(TargetSigningKey, e.SigningKey))
|
||||||
|
}
|
||||||
return handler.NewUpdateStatement(
|
return handler.NewUpdateStatement(
|
||||||
e,
|
e,
|
||||||
values,
|
values,
|
||||||
|
@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
testEvent(
|
testEvent(
|
||||||
target.AddedEventType,
|
target.AddedEventType,
|
||||||
target.AggregateType,
|
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],
|
eventstore.GenericEventMapper[target.AddedEvent],
|
||||||
),
|
),
|
||||||
@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
executer: &testExecuter{
|
executer: &testExecuter{
|
||||||
executions: []execution{
|
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{}{
|
expectedArgs: []interface{}{
|
||||||
"instance-id",
|
"instance-id",
|
||||||
"ro-id",
|
"ro-id",
|
||||||
@ -54,6 +54,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
domain.TargetTypeWebhook,
|
domain.TargetTypeWebhook,
|
||||||
3 * time.Second,
|
3 * time.Second,
|
||||||
true,
|
true,
|
||||||
|
anyArg{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -67,7 +68,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
testEvent(
|
testEvent(
|
||||||
target.ChangedEventType,
|
target.ChangedEventType,
|
||||||
target.AggregateType,
|
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],
|
eventstore.GenericEventMapper[target.ChangedEvent],
|
||||||
),
|
),
|
||||||
@ -79,7 +80,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
executer: &testExecuter{
|
executer: &testExecuter{
|
||||||
executions: []execution{
|
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{}{
|
expectedArgs: []interface{}{
|
||||||
anyArg{},
|
anyArg{},
|
||||||
uint64(15),
|
uint64(15),
|
||||||
@ -89,6 +90,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
"https://example.com",
|
"https://example.com",
|
||||||
3 * time.Second,
|
3 * time.Second,
|
||||||
true,
|
true,
|
||||||
|
anyArg{},
|
||||||
"instance-id",
|
"instance-id",
|
||||||
"agg-id",
|
"agg-id",
|
||||||
},
|
},
|
||||||
@ -116,7 +118,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
executer: &testExecuter{
|
executer: &testExecuter{
|
||||||
executions: []execution{
|
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{}{
|
expectedArgs: []interface{}{
|
||||||
"instance-id",
|
"instance-id",
|
||||||
"agg-id",
|
"agg-id",
|
||||||
@ -145,7 +147,7 @@ func TestTargetProjection_reduces(t *testing.T) {
|
|||||||
executer: &testExecuter{
|
executer: &testExecuter{
|
||||||
executions: []execution{
|
executions: []execution{
|
||||||
{
|
{
|
||||||
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1)",
|
expectedStmt: "DELETE FROM projections.targets2 WHERE (instance_id = $1)",
|
||||||
expectedArgs: []interface{}{
|
expectedArgs: []interface{}{
|
||||||
"agg-id",
|
"agg-id",
|
||||||
},
|
},
|
||||||
|
@ -31,6 +31,7 @@ type Queries struct {
|
|||||||
|
|
||||||
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
|
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
|
||||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||||
|
targetEncryptionAlgorithm crypto.EncryptionAlgorithm
|
||||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
|
||||||
checkPermission domain.PermissionCheck
|
checkPermission domain.PermissionCheck
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ func StartQueries(
|
|||||||
cacheConnectors connector.Connectors,
|
cacheConnectors connector.Connectors,
|
||||||
projections projection.Config,
|
projections projection.Config,
|
||||||
defaults sd.SystemDefaults,
|
defaults sd.SystemDefaults,
|
||||||
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm, targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||||
zitadelRoles []authz.RoleMapping,
|
zitadelRoles []authz.RoleMapping,
|
||||||
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
|
||||||
permissionCheck func(q *Queries) domain.PermissionCheck,
|
permissionCheck func(q *Queries) domain.PermissionCheck,
|
||||||
@ -70,6 +71,7 @@ func StartQueries(
|
|||||||
zitadelRoles: zitadelRoles,
|
zitadelRoles: zitadelRoles,
|
||||||
keyEncryptionAlgorithm: keyEncryptionAlgorithm,
|
keyEncryptionAlgorithm: keyEncryptionAlgorithm,
|
||||||
idpConfigEncryption: idpConfigEncryption,
|
idpConfigEncryption: idpConfigEncryption,
|
||||||
|
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
|
||||||
sessionTokenVerifier: sessionTokenVerifier,
|
sessionTokenVerifier: sessionTokenVerifier,
|
||||||
multifactors: domain.MultifactorConfigs{
|
multifactors: domain.MultifactorConfigs{
|
||||||
OTP: domain.OTPConfig{
|
OTP: domain.OTPConfig{
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/query/projection"
|
"github.com/zitadel/zitadel/internal/query/projection"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
@ -59,6 +60,10 @@ var (
|
|||||||
name: projection.TargetInterruptOnErrorCol,
|
name: projection.TargetInterruptOnErrorCol,
|
||||||
table: targetTable,
|
table: targetTable,
|
||||||
}
|
}
|
||||||
|
TargetColumnSigningKey = Column{
|
||||||
|
name: projection.TargetSigningKey,
|
||||||
|
table: targetTable,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Targets struct {
|
type Targets struct {
|
||||||
@ -78,6 +83,20 @@ type Target struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
InterruptOnError bool
|
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 {
|
type TargetSearchQueries struct {
|
||||||
@ -93,21 +112,37 @@ func (q *TargetSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
|||||||
return query
|
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{
|
eq := sq.Eq{
|
||||||
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||||
}
|
}
|
||||||
query, scan := prepareTargetsQuery(ctx, q.client)
|
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{
|
eq := sq.Eq{
|
||||||
TargetColumnID.identifier(): id,
|
TargetColumnID.identifier(): id,
|
||||||
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||||
}
|
}
|
||||||
query, scan := prepareTargetQuery(ctx, q.client)
|
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) {
|
func NewTargetNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
|
||||||
@ -129,6 +164,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu
|
|||||||
TargetColumnTimeout.identifier(),
|
TargetColumnTimeout.identifier(),
|
||||||
TargetColumnURL.identifier(),
|
TargetColumnURL.identifier(),
|
||||||
TargetColumnInterruptOnError.identifier(),
|
TargetColumnInterruptOnError.identifier(),
|
||||||
|
TargetColumnSigningKey.identifier(),
|
||||||
countColumn.identifier(),
|
countColumn.identifier(),
|
||||||
).From(targetTable.identifier()).
|
).From(targetTable.identifier()).
|
||||||
PlaceholderFormat(sq.Dollar),
|
PlaceholderFormat(sq.Dollar),
|
||||||
@ -147,6 +183,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu
|
|||||||
&target.Timeout,
|
&target.Timeout,
|
||||||
&target.Endpoint,
|
&target.Endpoint,
|
||||||
&target.InterruptOnError,
|
&target.InterruptOnError,
|
||||||
|
&target.signingKey,
|
||||||
&count,
|
&count,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -179,6 +216,7 @@ func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fun
|
|||||||
TargetColumnTimeout.identifier(),
|
TargetColumnTimeout.identifier(),
|
||||||
TargetColumnURL.identifier(),
|
TargetColumnURL.identifier(),
|
||||||
TargetColumnInterruptOnError.identifier(),
|
TargetColumnInterruptOnError.identifier(),
|
||||||
|
TargetColumnSigningKey.identifier(),
|
||||||
).From(targetTable.identifier()).
|
).From(targetTable.identifier()).
|
||||||
PlaceholderFormat(sq.Dollar),
|
PlaceholderFormat(sq.Dollar),
|
||||||
func(row *sql.Row) (*Target, error) {
|
func(row *sql.Row) (*Target, error) {
|
||||||
@ -193,6 +231,7 @@ func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fun
|
|||||||
&target.Timeout,
|
&target.Timeout,
|
||||||
&target.Endpoint,
|
&target.Endpoint,
|
||||||
&target.InterruptOnError,
|
&target.InterruptOnError,
|
||||||
|
&target.signingKey,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
@ -9,22 +9,24 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
prepareTargetsStmt = `SELECT projections.targets1.id,` +
|
prepareTargetsStmt = `SELECT projections.targets2.id,` +
|
||||||
` projections.targets1.creation_date,` +
|
` projections.targets2.creation_date,` +
|
||||||
` projections.targets1.change_date,` +
|
` projections.targets2.change_date,` +
|
||||||
` projections.targets1.resource_owner,` +
|
` projections.targets2.resource_owner,` +
|
||||||
` projections.targets1.name,` +
|
` projections.targets2.name,` +
|
||||||
` projections.targets1.target_type,` +
|
` projections.targets2.target_type,` +
|
||||||
` projections.targets1.timeout,` +
|
` projections.targets2.timeout,` +
|
||||||
` projections.targets1.endpoint,` +
|
` projections.targets2.endpoint,` +
|
||||||
` projections.targets1.interrupt_on_error,` +
|
` projections.targets2.interrupt_on_error,` +
|
||||||
|
` projections.targets2.signing_key,` +
|
||||||
` COUNT(*) OVER ()` +
|
` COUNT(*) OVER ()` +
|
||||||
` FROM projections.targets1`
|
` FROM projections.targets2`
|
||||||
prepareTargetsCols = []string{
|
prepareTargetsCols = []string{
|
||||||
"id",
|
"id",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
@ -35,19 +37,21 @@ var (
|
|||||||
"timeout",
|
"timeout",
|
||||||
"endpoint",
|
"endpoint",
|
||||||
"interrupt_on_error",
|
"interrupt_on_error",
|
||||||
|
"signing_key",
|
||||||
"count",
|
"count",
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareTargetStmt = `SELECT projections.targets1.id,` +
|
prepareTargetStmt = `SELECT projections.targets2.id,` +
|
||||||
` projections.targets1.creation_date,` +
|
` projections.targets2.creation_date,` +
|
||||||
` projections.targets1.change_date,` +
|
` projections.targets2.change_date,` +
|
||||||
` projections.targets1.resource_owner,` +
|
` projections.targets2.resource_owner,` +
|
||||||
` projections.targets1.name,` +
|
` projections.targets2.name,` +
|
||||||
` projections.targets1.target_type,` +
|
` projections.targets2.target_type,` +
|
||||||
` projections.targets1.timeout,` +
|
` projections.targets2.timeout,` +
|
||||||
` projections.targets1.endpoint,` +
|
` projections.targets2.endpoint,` +
|
||||||
` projections.targets1.interrupt_on_error` +
|
` projections.targets2.interrupt_on_error,` +
|
||||||
` FROM projections.targets1`
|
` projections.targets2.signing_key` +
|
||||||
|
` FROM projections.targets2`
|
||||||
prepareTargetCols = []string{
|
prepareTargetCols = []string{
|
||||||
"id",
|
"id",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
@ -58,6 +62,7 @@ var (
|
|||||||
"timeout",
|
"timeout",
|
||||||
"endpoint",
|
"endpoint",
|
||||||
"interrupt_on_error",
|
"interrupt_on_error",
|
||||||
|
"signing_key",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -102,6 +107,12 @@ func Test_TargetPrepares(t *testing.T) {
|
|||||||
1 * time.Second,
|
1 * time.Second,
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
true,
|
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,
|
Timeout: 1 * time.Second,
|
||||||
Endpoint: "https://example.com",
|
Endpoint: "https://example.com",
|
||||||
InterruptOnError: true,
|
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,
|
1 * time.Second,
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
true,
|
true,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "alg",
|
||||||
|
KeyID: "encKey",
|
||||||
|
Crypted: []byte("crypted"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id-2",
|
"id-2",
|
||||||
@ -156,6 +179,12 @@ func Test_TargetPrepares(t *testing.T) {
|
|||||||
1 * time.Second,
|
1 * time.Second,
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
false,
|
false,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "alg",
|
||||||
|
KeyID: "encKey",
|
||||||
|
Crypted: []byte("crypted"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id-3",
|
"id-3",
|
||||||
@ -167,6 +196,12 @@ func Test_TargetPrepares(t *testing.T) {
|
|||||||
1 * time.Second,
|
1 * time.Second,
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
false,
|
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,
|
Timeout: 1 * time.Second,
|
||||||
Endpoint: "https://example.com",
|
Endpoint: "https://example.com",
|
||||||
InterruptOnError: true,
|
InterruptOnError: true,
|
||||||
|
signingKey: &crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "alg",
|
||||||
|
KeyID: "encKey",
|
||||||
|
Crypted: []byte("crypted"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ObjectDetails: domain.ObjectDetails{
|
ObjectDetails: domain.ObjectDetails{
|
||||||
@ -201,6 +242,12 @@ func Test_TargetPrepares(t *testing.T) {
|
|||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
Endpoint: "https://example.com",
|
Endpoint: "https://example.com",
|
||||||
InterruptOnError: false,
|
InterruptOnError: false,
|
||||||
|
signingKey: &crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "alg",
|
||||||
|
KeyID: "encKey",
|
||||||
|
Crypted: []byte("crypted"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ObjectDetails: domain.ObjectDetails{
|
ObjectDetails: domain.ObjectDetails{
|
||||||
@ -214,6 +261,12 @@ func Test_TargetPrepares(t *testing.T) {
|
|||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
Endpoint: "https://example.com",
|
Endpoint: "https://example.com",
|
||||||
InterruptOnError: false,
|
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,
|
1 * time.Second,
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
true,
|
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,
|
Timeout: 1 * time.Second,
|
||||||
Endpoint: "https://example.com",
|
Endpoint: "https://example.com",
|
||||||
InterruptOnError: true,
|
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
|
ON e.instance_id = p.instance_id
|
||||||
AND e.include IS NOT NULL
|
AND e.include IS NOT NULL
|
||||||
AND e.include = p.execution_id)
|
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
|
FROM dissolved_execution_targets e
|
||||||
JOIN projections.targets1 t
|
JOIN projections.targets2 t
|
||||||
ON e.instance_id = t.instance_id
|
ON e.instance_id = t.instance_id
|
||||||
AND e.target_id = t.id
|
AND e.target_id = t.id
|
||||||
WHERE "include" = ''
|
WHERE "include" = ''
|
||||||
|
@ -38,9 +38,9 @@ WITH RECURSIVE
|
|||||||
ON e.instance_id = p.instance_id
|
ON e.instance_id = p.instance_id
|
||||||
AND e.include IS NOT NULL
|
AND e.include IS NOT NULL
|
||||||
AND e.include = p.execution_id)
|
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
|
FROM dissolved_execution_targets e
|
||||||
JOIN projections.targets1 t
|
JOIN projections.targets2 t
|
||||||
ON e.instance_id = t.instance_id
|
ON e.instance_id = t.instance_id
|
||||||
AND e.target_id = t.id
|
AND e.target_id = t.id
|
||||||
WHERE "include" = ''
|
WHERE "include" = ''
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
)
|
)
|
||||||
@ -23,6 +24,7 @@ type AddedEvent struct {
|
|||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
Timeout time.Duration `json:"timeout"`
|
Timeout time.Duration `json:"timeout"`
|
||||||
InterruptOnError bool `json:"interruptOnError"`
|
InterruptOnError bool `json:"interruptOnError"`
|
||||||
|
SigningKey *crypto.CryptoValue `json:"signingKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
|
||||||
@ -45,12 +47,13 @@ func NewAddedEvent(
|
|||||||
endpoint string,
|
endpoint string,
|
||||||
timeout time.Duration,
|
timeout time.Duration,
|
||||||
interruptOnError bool,
|
interruptOnError bool,
|
||||||
|
signingKey *crypto.CryptoValue,
|
||||||
) *AddedEvent {
|
) *AddedEvent {
|
||||||
return &AddedEvent{
|
return &AddedEvent{
|
||||||
*eventstore.NewBaseEventForPush(
|
*eventstore.NewBaseEventForPush(
|
||||||
ctx, aggregate, AddedEventType,
|
ctx, aggregate, AddedEventType,
|
||||||
),
|
),
|
||||||
name, targetType, endpoint, timeout, interruptOnError}
|
name, targetType, endpoint, timeout, interruptOnError, signingKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangedEvent struct {
|
type ChangedEvent struct {
|
||||||
@ -61,6 +64,7 @@ type ChangedEvent struct {
|
|||||||
Endpoint *string `json:"endpoint,omitempty"`
|
Endpoint *string `json:"endpoint,omitempty"`
|
||||||
Timeout *time.Duration `json:"timeout,omitempty"`
|
Timeout *time.Duration `json:"timeout,omitempty"`
|
||||||
InterruptOnError *bool `json:"interruptOnError,omitempty"`
|
InterruptOnError *bool `json:"interruptOnError,omitempty"`
|
||||||
|
SigningKey *crypto.CryptoValue `json:"signingKey,omitempty"`
|
||||||
|
|
||||||
oldName string
|
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 {
|
type RemovedEvent struct {
|
||||||
eventstore.BaseEvent `json:"-"`
|
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 {
|
message CreateTargetResponse {
|
||||||
zitadel.resources.object.v3alpha.Details details = 1;
|
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 {
|
message PatchTargetRequest {
|
||||||
@ -433,6 +439,12 @@ message PatchTargetRequest {
|
|||||||
|
|
||||||
message PatchTargetResponse {
|
message PatchTargetResponse {
|
||||||
zitadel.resources.object.v3alpha.Details details = 1;
|
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 {
|
message DeleteTargetRequest {
|
||||||
|
@ -9,6 +9,7 @@ import "google/protobuf/struct.proto";
|
|||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
import "zitadel/protoc_gen_zitadel/v2/options.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
import "zitadel/resources/object/v3alpha/object.proto";
|
import "zitadel/resources/object/v3alpha/object.proto";
|
||||||
|
|
||||||
@ -51,6 +52,11 @@ message Target {
|
|||||||
message GetTarget {
|
message GetTarget {
|
||||||
zitadel.resources.object.v3alpha.Details details = 1;
|
zitadel.resources.object.v3alpha.Details details = 1;
|
||||||
Target config = 2;
|
Target config = 2;
|
||||||
|
string signing_key = 3 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"98KmsU67\""
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
message PatchTarget {
|
message PatchTarget {
|
||||||
@ -84,6 +90,21 @@ message PatchTarget {
|
|||||||
max_length: 1000
|
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