Merge branch 'main' into notification-worker-fix

This commit is contained in:
Livio Spring 2024-12-02 07:27:17 +01:00 committed by GitHub
commit 3e05d6c3d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1075 additions and 126 deletions

View File

@ -8,8 +8,9 @@ The following is a directory of adopters to help identify users of individual fe
If you are using Zitadel, please consider adding yourself as a user with a quick description of your use case by opening a pull request to this file and adding a section describing your usage of Zitadel.
| Organization/Individual | Contact Information | Description of Usage |
| ----------------------- | -------------------------------------------------------- | ----------------------------------------------- |
| Zitadel | [@fforootd](https://github.com/fforootd) (and many more) | Zitadel Cloud makes heavy use of of Zitadel ;-) |
| Organization/Individual | Contact Information | Description of Usage |
| ----------------------- | -------------------------------------------------------------------- | ----------------------------------------------- |
| Zitadel | [@fforootd](https://github.com/fforootd) (and many more) | Zitadel Cloud makes heavy use of of Zitadel ;-) |
| Rawkode Academy | [@RawkodeAcademy](https://github.com/RawkodeAcademy) | Rawkode Academy Platform & Zulip use Zitadel for all user and M2M authentication |
| Organization Name | contact@example.com | Description of how they use Zitadel |
| Individual Name | contact@example.com | Description of how they use Zitadel |

View File

@ -633,6 +633,9 @@ EncryptionKeys:
User:
EncryptionKeyID: "userKey" # ZITADEL_ENCRYPTIONKEYS_USER_ENCRYPTIONKEYID
DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_USER_DECRYPTIONKEYIDS (comma separated list)
Target:
EncryptionKeyID: "targetKey" # ZITADEL_ENCRYPTIONKEYS_TARGET_ENCRYPTIONKEYID
DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_TARGET_DECRYPTIONKEYIDS (comma separated list)
CSRFCookieKeyID: "csrfCookieKey" # ZITADEL_ENCRYPTIONKEYS_CSRFCOOKIEKEYID
UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID
@ -910,6 +913,12 @@ DefaultInstance:
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEUPPERLETTERS
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEDIGITS
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDESYMBOLS
SigningKey:
Length: 36 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_LENGTH
IncludeLowerLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDELOWERLETTERS
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDEUPPERLETTERS
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDEDIGITS
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_SIGNINGKEY_INCLUDESYMBOLS
PasswordComplexityPolicy:
MinLength: 8 # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_MINLENGTH
HasLowercase: true # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_HASLOWERCASE

View File

@ -17,6 +17,7 @@ var (
"smsKey",
"smtpKey",
"userKey",
"targetKey",
"csrfCookieKey",
"userAgentCookieKey",
}
@ -31,6 +32,7 @@ type EncryptionKeyConfig struct {
SMS *crypto.KeyConfig
SMTP *crypto.KeyConfig
User *crypto.KeyConfig
Target *crypto.KeyConfig
CSRFCookieKeyID string
UserAgentCookieKeyID string
}
@ -44,6 +46,7 @@ type EncryptionKeys struct {
SMS crypto.EncryptionAlgorithm
SMTP crypto.EncryptionAlgorithm
User crypto.EncryptionAlgorithm
Target crypto.EncryptionAlgorithm
CSRFCookieKey []byte
UserAgentCookieKey []byte
OIDCKey []byte
@ -91,6 +94,10 @@ func EnsureEncryptionKeys(ctx context.Context, keyConfig *EncryptionKeyConfig, k
if err != nil {
return nil, err
}
keys.Target, err = crypto.NewAESCrypto(keyConfig.Target, keyStorage)
if err != nil {
return nil, err
}
key, err = crypto.LoadKey(keyConfig.CSRFCookieKeyID, keyStorage)
if err != nil {
return nil, err

View File

@ -145,6 +145,7 @@ func projections(
keys.OTP,
keys.OIDC,
keys.SAML,
keys.Target,
config.InternalAuthZ.RolePermissionMappings,
sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck {
@ -183,6 +184,7 @@ func projections(
keys.DomainVerification,
keys.OIDC,
keys.SAML,
keys.Target,
&http.Client{},
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)

View File

@ -86,6 +86,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
nil,
nil,
nil,
nil,
0,
0,
0,

View File

@ -53,6 +53,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
nil,
nil,
nil,
nil,
0,
0,
0,

View File

@ -366,6 +366,7 @@ func initProjections(
keys.OTP,
keys.OIDC,
keys.SAML,
keys.Target,
config.InternalAuthZ.RolePermissionMappings,
sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck {
@ -422,6 +423,7 @@ func initProjections(
keys.DomainVerification,
keys.OIDC,
keys.SAML,
keys.Target,
&http.Client{},
permissionCheck,
sessionTokenVerifier,

View File

@ -196,6 +196,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
keys.OTP,
keys.OIDC,
keys.SAML,
keys.Target,
config.InternalAuthZ.RolePermissionMappings,
sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck {
@ -245,6 +246,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
keys.DomainVerification,
keys.OIDC,
keys.SAML,
keys.Target,
&http.Client{},
permissionCheck,
sessionTokenVerifier,

View File

@ -48,6 +48,20 @@ func main() {
What happens here is only a target which prints out the received request, which could also be handled with a different logic.
### Check Signature
To additionally check the signature header you can add the following to the example:
```go
// validate signature
if err := actions.ValidatePayload(sentBody, req.Header.Get(actions.SigningHeader), signingKey); err != nil {
// if the signed content is not equal the sent content return an error
http.Error(w, "error", http.StatusInternalServerError)
return
}
```
Where you can replace 'signingKey' with the key received in the next step 'Create target'.
## Create target
As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows:

View File

@ -64,6 +64,13 @@ There are different types of Targets:
The API documentation to create a target can be found [here](/apis/resources/action_service_v3/zitadel-actions-create-target)
### Content Signing
To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical.
Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v3/zitadel-actions-create-target),
and can also be newly generated when a Target is [patched](/apis/resources/action_service_v3/zitadel-actions-patch-target).
## Execution
ZITADEL decides on specific conditions if one or more Targets have to be called.

View File

@ -17,7 +17,7 @@
"generate:grpc": "buf generate ../proto",
"generate:apidocs": "docusaurus gen-api-docs all",
"generate:configdocs": "cp -r ../cmd/defaults.yaml ./docs/self-hosting/manage/configure/ && cp -r ../cmd/setup/steps.yaml ./docs/self-hosting/manage/configure/",
"generate:re-gen": "yarn clean-all && yarn gen-all",
"generate:re-gen": "yarn generate:clean-all && yarn generate",
"generate:clean-all": "docusaurus clean-api-docs all"
},
"dependencies": {

View File

@ -62,6 +62,7 @@ func TestServer_GetTarget(t *testing.T) {
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
@ -92,6 +93,7 @@ func TestServer_GetTarget(t *testing.T) {
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
@ -122,6 +124,7 @@ func TestServer_GetTarget(t *testing.T) {
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
@ -154,6 +157,7 @@ func TestServer_GetTarget(t *testing.T) {
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
@ -186,6 +190,7 @@ func TestServer_GetTarget(t *testing.T) {
request.Id = resp.GetDetails().GetId()
response.Target.Config.Name = name
response.Target.Details = resp.GetDetails()
response.Target.SigningKey = resp.GetSigningKey()
return nil
},
req: &action.GetTargetRequest{},
@ -230,6 +235,7 @@ func TestServer_GetTarget(t *testing.T) {
gotTarget := got.GetTarget()
integration.AssertResourceDetails(ttt, wantTarget.GetDetails(), gotTarget.GetDetails())
assert.EqualExportedValues(ttt, wantTarget.GetConfig(), gotTarget.GetConfig())
assert.Equal(ttt, wantTarget.GetSigningKey(), gotTarget.GetSigningKey())
}, retryDuration, tick, "timeout waiting for expected target result")
})
}
@ -492,6 +498,7 @@ func TestServer_ListTargets(t *testing.T) {
for i := range tt.want.Result {
integration.AssertResourceDetails(ttt, tt.want.Result[i].GetDetails(), got.Result[i].GetDetails())
assert.EqualExportedValues(ttt, tt.want.Result[i].GetConfig(), got.Result[i].GetConfig())
assert.NotEmpty(ttt, got.Result[i].GetSigningKey())
}
}
integration.AssertResourceListDetails(ttt, tt.want, got)

View File

@ -9,6 +9,7 @@ import (
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
@ -200,11 +201,12 @@ func TestServer_CreateTarget(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
require.Error(t, err)
return
assert.Error(t, err)
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
assert.NotEmpty(t, got.GetSigningKey())
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
@ -217,11 +219,15 @@ func TestServer_PatchTarget(t *testing.T) {
ctx context.Context
req *action.PatchTargetRequest
}
type want struct {
details *resource_object.Details
signingKey bool
}
tests := []struct {
name string
prepare func(request *action.PatchTargetRequest) error
args args
want *resource_object.Details
want want
wantErr bool
}{
{
@ -272,14 +278,42 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
{
name: "regenerate signingkey, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: isolatedIAMOwnerCTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
ExpirationSigningKey: durationpb.New(0 * time.Second),
},
},
},
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
signingKey: true,
},
},
{
name: "change type, ok",
prepare: func(request *action.PatchTargetRequest) error {
@ -299,11 +333,13 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
@ -322,11 +358,13 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
@ -345,11 +383,13 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
@ -370,11 +410,13 @@ func TestServer_PatchTarget(t *testing.T) {
},
},
},
want: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
want: want{
details: &resource_object.Details{
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: instance.ID(),
},
},
},
},
@ -387,11 +429,14 @@ func TestServer_PatchTarget(t *testing.T) {
instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
got, err := instance.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
assert.Error(t, err)
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want.details, got.Details)
if tt.want.signingKey {
assert.NotEmpty(t, got.SigningKey)
}
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
@ -443,11 +488,12 @@ func TestServer_DeleteTarget(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got, err := instance.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
assert.Error(t, err)
return
} else {
assert.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}

View File

@ -97,6 +97,7 @@ func targetToPb(t *query.Target) *action.GetTarget {
Timeout: durationpb.New(t.Timeout),
Endpoint: t.Endpoint,
},
SigningKey: t.SigningKey,
}
switch t.TargetType {
case domain.TargetTypeWebhook:

View File

@ -25,7 +25,8 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque
return nil, err
}
return &action.CreateTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SigningKey: add.SigningKey,
}, nil
}
@ -34,12 +35,14 @@ func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest
return nil, err
}
instanceID := authz.GetInstance(ctx).InstanceID()
details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instanceID)
patch := patchTargetToCommand(req)
details, err := s.command.ChangeTarget(ctx, patch, instanceID)
if err != nil {
return nil, err
}
return &action.PatchTargetResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
SigningKey: patch.SigningKey,
}, nil
}
@ -83,6 +86,12 @@ func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
}
func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget {
expirationSigningKey := false
// TODO handle expiration, currently only immediate expiration is supported
if req.GetTarget().GetExpirationSigningKey() != nil {
expirationSigningKey = true
}
reqTarget := req.GetTarget()
if reqTarget == nil {
return nil
@ -91,8 +100,9 @@ func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: reqTarget.Name,
Endpoint: reqTarget.Endpoint,
Name: reqTarget.Name,
Endpoint: reqTarget.Endpoint,
ExpirationSigningKey: expirationSigningKey,
}
if reqTarget.TargetType != nil {
switch t := reqTarget.GetTargetType().(type) {

View File

@ -26,6 +26,7 @@ type mockExecutionTarget struct {
Endpoint string
Timeout time.Duration
InterruptOnError bool
SigningKey string
}
func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
@ -49,6 +50,9 @@ func (e *mockExecutionTarget) GetTargetID() string {
func (e *mockExecutionTarget) GetExecutionID() string {
return e.ExecutionID
}
func (e *mockExecutionTarget) GetSigningKey() string {
return e.SigningKey
}
type mockContentRequest struct {
Content string
@ -157,6 +161,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
SigningKey: "signingkey",
},
},
targets: []target{
@ -186,6 +191,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
@ -216,6 +222,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -245,6 +252,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -269,6 +277,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -297,6 +306,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Second,
SigningKey: "signingkey",
},
},
targets: []target{
@ -325,6 +335,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Minute,
SigningKey: "signingkey",
},
},
targets: []target{
@ -354,6 +365,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -382,6 +394,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -411,6 +424,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -440,6 +454,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
&mockExecutionTarget{
InstanceID: "instance",
@ -448,6 +463,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
&mockExecutionTarget{
InstanceID: "instance",
@ -456,6 +472,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
@ -498,6 +515,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
&mockExecutionTarget{
InstanceID: "instance",
@ -506,6 +524,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
SigningKey: "signingkey",
},
&mockExecutionTarget{
InstanceID: "instance",
@ -514,6 +533,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -692,6 +712,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{
@ -721,6 +742,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
SigningKey: "signingkey",
},
},
targets: []target{

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/execution"
@ -172,6 +173,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
"https://example.com",
time.Second,
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@ -221,6 +228,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
"https://example.com",
time.Second,
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@ -270,6 +283,12 @@ func TestCommands_SetExecutionRequest(t *testing.T) {
"https://example.com",
time.Second,
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@ -836,6 +855,12 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
"https://example.com",
time.Second,
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
expectPushFailed(
@ -930,6 +955,12 @@ func TestCommands_SetExecutionResponse(t *testing.T) {
"https://example.com",
time.Second,
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),

View File

@ -5,6 +5,8 @@ import (
"net/url"
"time"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/target"
@ -19,6 +21,8 @@ type AddTarget struct {
Endpoint string
Timeout time.Duration
InterruptOnError bool
SigningKey string
}
func (a *AddTarget) IsValid() error {
@ -58,7 +62,11 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
if wm.State.Exists() {
return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-9axkz0jvzm", "Errors.Target.AlreadyExists")
}
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint
if err != nil {
return nil, err
}
add.SigningKey = code.PlainCode()
pushedEvents, err := c.eventstore.Push(ctx, target.NewAddedEvent(
ctx,
TargetAggregateFromWriteModel(&wm.WriteModel),
@ -67,6 +75,7 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
add.Endpoint,
add.Timeout,
add.InterruptOnError,
code.Crypted,
))
if err != nil {
return nil, err
@ -85,6 +94,9 @@ type ChangeTarget struct {
Endpoint *string
Timeout *time.Duration
InterruptOnError *bool
ExpirationSigningKey bool
SigningKey *string
}
func (a *ChangeTarget) IsValid() error {
@ -120,6 +132,17 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
if !existing.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-xj14f2cccn", "Errors.Target.NotFound")
}
var changedSigningKey *crypto.CryptoValue
if change.ExpirationSigningKey {
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.targetEncryption) //nolint
if err != nil {
return nil, err
}
changedSigningKey = code.Crypted
change.SigningKey = &code.Plain
}
changedEvent := existing.NewChangedEvent(
ctx,
TargetAggregateFromWriteModel(&existing.WriteModel),
@ -127,7 +150,9 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
change.TargetType,
change.Endpoint,
change.Timeout,
change.InterruptOnError)
change.InterruptOnError,
changedSigningKey,
)
if changedEvent == nil {
return writeModelToObjectDetails(&existing.WriteModel), nil
}
@ -184,3 +209,7 @@ func (c *Commands) getTargetWriteModelByID(ctx context.Context, id string, resou
}
return wm, nil
}
func (c *Commands) newSigningKey(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) {
return c.newEncryptedCodeWithDefault(ctx, filter, domain.SecretGeneratorTypeSigningKey, alg, c.defaultSecretGenerators.SigningKey)
}

View File

@ -5,6 +5,7 @@ import (
"slices"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/target"
@ -18,6 +19,7 @@ type TargetWriteModel struct {
Endpoint string
Timeout time.Duration
InterruptOnError bool
SigningKey *crypto.CryptoValue
State domain.TargetState
}
@ -41,6 +43,7 @@ func (wm *TargetWriteModel) Reduce() error {
wm.Endpoint = e.Endpoint
wm.Timeout = e.Timeout
wm.State = domain.TargetActive
wm.SigningKey = e.SigningKey
case *target.ChangedEvent:
if e.Name != nil {
wm.Name = *e.Name
@ -57,6 +60,9 @@ func (wm *TargetWriteModel) Reduce() error {
if e.InterruptOnError != nil {
wm.InterruptOnError = *e.InterruptOnError
}
if e.SigningKey != nil {
wm.SigningKey = e.SigningKey
}
case *target.RemovedEvent:
wm.State = domain.TargetRemoved
}
@ -84,6 +90,7 @@ func (wm *TargetWriteModel) NewChangedEvent(
endpoint *string,
timeout *time.Duration,
interruptOnError *bool,
signingKey *crypto.CryptoValue,
) *target.ChangedEvent {
changes := make([]target.Changes, 0)
if name != nil && wm.Name != *name {
@ -101,6 +108,10 @@ func (wm *TargetWriteModel) NewChangedEvent(
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
changes = append(changes, target.ChangeInterruptOnError(*interruptOnError))
}
// if signingkey is set, update it as it is encrypted
if signingKey != nil {
changes = append(changes, target.ChangeSigningKey(signingKey))
}
if len(changes) == 0 {
return nil
}

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/target"
@ -20,6 +21,12 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
"https://example.com",
time.Second,
false,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
)
}

View File

@ -8,6 +8,7 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@ -19,8 +20,10 @@ import (
func TestCommands_AddTarget(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
}
type args struct {
ctx context.Context
@ -132,10 +135,18 @@ func TestCommands_AddTarget(t *testing.T) {
"https://example.com",
time.Second,
false,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
idGenerator: mock.ExpectID(t, "id1"),
idGenerator: mock.ExpectID(t, "id1"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args{
ctx: context.Background(),
@ -186,7 +197,9 @@ func TestCommands_AddTarget(t *testing.T) {
targetAddEvent("id1", "instance"),
),
),
idGenerator: mock.ExpectID(t, "id1"),
idGenerator: mock.ExpectID(t, "id1"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args{
ctx: context.Background(),
@ -219,7 +232,9 @@ func TestCommands_AddTarget(t *testing.T) {
}(),
),
),
idGenerator: mock.ExpectID(t, "id1"),
idGenerator: mock.ExpectID(t, "id1"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args{
ctx: context.Background(),
@ -244,8 +259,10 @@ func TestCommands_AddTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
}
details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
if tt.res.err == nil {
@ -264,7 +281,9 @@ func TestCommands_AddTarget(t *testing.T) {
func TestCommands_ChangeTarget(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
}
type args struct {
ctx context.Context
@ -510,10 +529,18 @@ func TestCommands_ChangeTarget(t *testing.T) {
target.ChangeTargetType(domain.TargetTypeCall),
target.ChangeTimeout(10 * time.Second),
target.ChangeInterruptOnError(true),
target.ChangeSigningKey(&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
}),
},
),
),
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args{
ctx: context.Background(),
@ -521,11 +548,12 @@ func TestCommands_ChangeTarget(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "id1",
},
Name: gu.Ptr("name2"),
Endpoint: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
Name: gu.Ptr("name2"),
Endpoint: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
ExpirationSigningKey: true,
},
resourceOwner: "instance",
},
@ -540,7 +568,9 @@ func TestCommands_ChangeTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
}
details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
if tt.res.err == nil {

View File

@ -54,6 +54,7 @@ type Commands struct {
smtpEncryption crypto.EncryptionAlgorithm
smsEncryption crypto.EncryptionAlgorithm
userEncryption crypto.EncryptionAlgorithm
targetEncryption crypto.EncryptionAlgorithm
userPasswordHasher *crypto.Hasher
secretHasher *crypto.Hasher
machineKeySize int
@ -108,7 +109,7 @@ func StartCommands(
externalDomain string,
externalSecure bool,
externalPort uint16,
idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption crypto.EncryptionAlgorithm,
idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption, targetEncryption crypto.EncryptionAlgorithm,
httpClient *http.Client,
permissionCheck domain.PermissionCheck,
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
@ -153,6 +154,7 @@ func StartCommands(
smtpEncryption: smtpEncryption,
smsEncryption: smsEncryption,
userEncryption: userEncryption,
targetEncryption: targetEncryption,
userPasswordHasher: userPasswordHasher,
secretHasher: secretHasher,
machineKeySize: int(defaults.SecretGenerators.MachineKeySize),

View File

@ -157,6 +157,7 @@ type SecretGenerators struct {
OTPSMS *crypto.GeneratorConfig
OTPEmail *crypto.GeneratorConfig
InviteCode *crypto.GeneratorConfig
SigningKey *crypto.GeneratorConfig
}
type ZitadelConfig struct {

View File

@ -202,6 +202,8 @@ func (wm *OrgDomainVerifiedWriteModel) AppendEvents(events ...eventstore.Event)
continue
}
wm.WriteModel.AppendEvents(e)
case *org.OrgRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
@ -214,6 +216,11 @@ func (wm *OrgDomainVerifiedWriteModel) Reduce() error {
wm.ResourceOwner = e.Aggregate().ResourceOwner
case *org.DomainRemovedEvent:
wm.Verified = false
case *org.OrgRemovedEvent:
if wm.ResourceOwner != e.Aggregate().ID {
continue
}
wm.Verified = false
}
}
return wm.WriteModel.Reduce()
@ -225,6 +232,7 @@ func (wm *OrgDomainVerifiedWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(org.AggregateType).
EventTypes(
org.OrgDomainVerifiedEventType,
org.OrgDomainRemovedEventType).
org.OrgDomainRemovedEventType,
org.OrgRemovedEventType).
Builder()
}

View File

@ -1725,6 +1725,323 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
wantID: "user1",
},
},
{
name: "register human (validate domain), already verified",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
false,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewDomainVerifiedEvent(context.Background(),
&org.NewAggregate("existing").Aggregate,
"example.com",
),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username@example.com",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@example.com",
},
PreferredLanguage: language.English,
Register: true,
UserAgentID: "userAgentID",
AuthRequestID: "authRequestID",
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername"))
},
},
},
{
name: "register human (validate domain), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
false,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewDomainVerifiedEvent(context.Background(),
&org.NewAggregate(userAgg.ResourceOwner).Aggregate,
"example.com",
),
),
),
expectPush(
user.NewHumanRegisteredEvent(context.Background(),
&userAgg.Aggregate,
"username@example.com",
"firstname",
"lastname",
"",
"firstname lastname",
language.English,
domain.GenderUnspecified,
"email@example.com",
false,
"userAgentID",
),
user.NewHumanInitialCodeAddedEvent(context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("userinit"),
},
time.Hour*1,
"authRequestID",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username@example.com",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@example.com",
},
PreferredLanguage: language.English,
Register: true,
UserAgentID: "userAgentID",
AuthRequestID: "authRequestID",
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "register human (validate domain, domain removed), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
false,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewDomainVerifiedEvent(context.Background(),
&org.NewAggregate("existing").Aggregate,
"example.com",
),
),
eventFromEventPusher(
org.NewDomainRemovedEvent(context.Background(),
&org.NewAggregate("existing").Aggregate,
"example.com",
true,
),
),
),
expectPush(
user.NewHumanRegisteredEvent(context.Background(),
&userAgg.Aggregate,
"username@example.com",
"firstname",
"lastname",
"",
"firstname lastname",
language.English,
domain.GenderUnspecified,
"email@example.com",
false,
"userAgentID",
),
user.NewHumanInitialCodeAddedEvent(context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("userinit"),
},
time.Hour*1,
"authRequestID",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username@example.com",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@example.com",
},
PreferredLanguage: language.English,
Register: true,
UserAgentID: "userAgentID",
AuthRequestID: "authRequestID",
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "register human (validate domain, org removed), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&userAgg.Aggregate,
false,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewDomainVerifiedEvent(context.Background(),
&org.NewAggregate("existing").Aggregate,
"example.com",
),
),
eventFromEventPusher(
org.NewOrgRemovedEvent(context.Background(),
&org.NewAggregate("existing").Aggregate,
"org",
[]string{},
false,
[]string{},
[]*domain.UserIDPLink{},
[]string{},
),
),
),
expectPush(
user.NewHumanRegisteredEvent(context.Background(),
&userAgg.Aggregate,
"username@example.com",
"firstname",
"lastname",
"",
"firstname lastname",
language.English,
domain.GenderUnspecified,
"email@example.com",
false,
"userAgentID",
),
user.NewHumanInitialCodeAddedEvent(context.Background(),
&userAgg.Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("userinit"),
},
time.Hour*1,
"authRequestID",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("userinit", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username@example.com",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@example.com",
},
PreferredLanguage: language.English,
Register: true,
UserAgentID: "userAgentID",
AuthRequestID: "authRequestID",
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: true,
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
want: &domain.ObjectDetails{
Sequence: 0,
EventDate: time.Time{},
ResourceOwner: "org1",
},
wantID: "user1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -15,6 +15,7 @@ const (
SecretGeneratorTypeOTPSMS
SecretGeneratorTypeOTPEmail
SecretGeneratorTypeInviteCode
SecretGeneratorTypeSigningKey
secretGeneratorTypeCount
)

View File

@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/actions"
)
type ContextInfo interface {
@ -28,6 +29,7 @@ type Target interface {
GetEndpoint() string
GetTargetType() domain.TargetType
GetTimeout() time.Duration
GetSigningKey() string
}
// CallTargets call a list of targets in order with handling of error and responses
@ -72,13 +74,13 @@ func CallTarget(
switch target.GetTargetType() {
// get request, ignore response and return request and error for handling in list of targets
case domain.TargetTypeWebhook:
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey())
// get request, return response and error
case domain.TargetTypeCall:
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey())
case domain.TargetTypeAsync:
go func(target Target, info ContextInfoRequest) {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody(), target.GetSigningKey()); err != nil {
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
}
}(target, info)
@ -89,13 +91,13 @@ func CallTarget(
}
// webhook call a webhook, ignore the response but return the errror
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error {
_, err := Call(ctx, url, timeout, body)
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) error {
_, err := Call(ctx, url, timeout, body, signingKey)
return err
}
// Call function to do a post HTTP request to a desired url with timeout
func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) {
func Call(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) (_ []byte, err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, span := tracing.NewSpan(ctx)
defer func() {
@ -108,6 +110,9 @@ func Call(ctx context.Context, url string, timeout time.Duration, body []byte) (
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if signingKey != "" {
req.Header.Set(actions.SigningHeader, actions.ComputeSignatureHeader(time.Now(), body, signingKey))
}
client := http.DefaultClient
resp, err := client.Do(req)

View File

@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
"github.com/zitadel/zitadel/internal/zerrors"
"github.com/zitadel/zitadel/pkg/actions"
)
func Test_Call(t *testing.T) {
@ -29,6 +30,7 @@ func Test_Call(t *testing.T) {
body []byte
respBody []byte
statusCode int
signingKey string
}
type res struct {
body []byte
@ -84,6 +86,22 @@ func Test_Call(t *testing.T) {
body: []byte("{\"response\": \"values\"}"),
},
},
{
"ok, signed",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
signingKey: "signingkey",
},
res{
body: []byte("{\"response\": \"values\"}"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -95,7 +113,7 @@ func Test_Call(t *testing.T) {
statusCode: tt.args.statusCode,
respondBody: tt.args.respBody,
},
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
testCall(tt.args.ctx, tt.args.timeout, tt.args.body, tt.args.signingKey),
)
if tt.res.wantErr {
assert.Error(t, err)
@ -186,6 +204,29 @@ func Test_CallTarget(t *testing.T) {
body: nil,
},
},
{
"webhook, signed, ok",
args{
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
timeout: time.Second,
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
signingKey: "signingkey",
},
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
SigningKey: "signingkey",
},
},
res{
body: nil,
},
},
{
"request response, error",
args{
@ -228,6 +269,29 @@ func Test_CallTarget(t *testing.T) {
body: []byte("{\"request\":\"content2\"}"),
},
},
{
"request response, signed, ok",
args{
ctx: context.Background(),
info: requestContextInfo1,
server: &callTestServer{
timeout: time.Second,
method: http.MethodPost,
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
respondBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
signingKey: "signingkey",
},
target: &mockTarget{
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
SigningKey: "signingkey",
},
},
res{
body: []byte("{\"request\":\"content2\"}"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -392,6 +456,7 @@ type mockTarget struct {
Endpoint string
Timeout time.Duration
InterruptOnError bool
SigningKey string
}
func (e *mockTarget) GetTargetID() string {
@ -409,6 +474,9 @@ func (e *mockTarget) GetTargetType() domain.TargetType {
func (e *mockTarget) GetTimeout() time.Duration {
return e.Timeout
}
func (e *mockTarget) GetSigningKey() string {
return e.SigningKey
}
type callTestServer struct {
method string
@ -416,6 +484,7 @@ type callTestServer struct {
timeout time.Duration
statusCode int
respondBody []byte
signingKey string
}
func testServers(
@ -447,7 +516,7 @@ func listen(
c *callTestServer,
) (url string, close func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
checkRequest(t, r, c.method, c.expectBody)
checkRequest(t, r, c.method, c.expectBody, c.signingKey)
if c.statusCode != http.StatusOK {
http.Error(w, "error", c.statusCode)
@ -466,16 +535,19 @@ func listen(
return server.URL, server.Close
}
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte, signingKey string) {
sentBody, err := io.ReadAll(sent.Body)
require.NoError(t, err)
require.Equal(t, expectedBody, sentBody)
require.Equal(t, method, sent.Method)
if signingKey != "" {
require.NoError(t, actions.ValidatePayload(sentBody, sent.Header.Get(actions.SigningHeader), signingKey))
}
}
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
func testCall(ctx context.Context, timeout time.Duration, body []byte, signingKey string) func(string) ([]byte, error) {
return func(url string) ([]byte, error) {
return execution.Call(ctx, url, timeout, body)
return execution.Call(ctx, url, timeout, body, signingKey)
}
}

View File

@ -11,6 +11,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query/projection"
@ -175,6 +176,11 @@ func (q *Queries) TargetsByExecutionID(ctx context.Context, ids []string) (execu
instanceID,
database.TextArray[string](ids),
)
for i := range execution {
if err := execution[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
return nil, err
}
}
return execution, err
}
@ -205,6 +211,11 @@ func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string
database.TextArray[string](ids1),
database.TextArray[string](ids2),
)
for i := range execution {
if err := execution[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
return nil, err
}
}
return execution, err
}
@ -352,6 +363,8 @@ type ExecutionTarget struct {
Endpoint string
Timeout time.Duration
InterruptOnError bool
signingKey *crypto.CryptoValue
SigningKey string
}
func (e *ExecutionTarget) GetExecutionID() string {
@ -372,6 +385,21 @@ func (e *ExecutionTarget) GetTargetType() domain.TargetType {
func (e *ExecutionTarget) GetTimeout() time.Duration {
return e.Timeout
}
func (e *ExecutionTarget) GetSigningKey() string {
return e.SigningKey
}
func (t *ExecutionTarget) decryptSigningKey(alg crypto.EncryptionAlgorithm) error {
if t.signingKey == nil {
return nil
}
keyValue, err := crypto.DecryptString(t.signingKey, alg)
if err != nil {
return zerrors.ThrowInternal(err, "QUERY-bxevy3YXwy", "Errors.Internal")
}
t.SigningKey = keyValue
return nil
}
func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
targets := make([]*ExecutionTarget, 0)
@ -386,6 +414,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
endpoint = &sql.NullString{}
timeout = &sql.NullInt64{}
interruptOnError = &sql.NullBool{}
signingKey = &crypto.CryptoValue{}
)
err := rows.Scan(
@ -396,6 +425,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
endpoint,
timeout,
interruptOnError,
signingKey,
)
if err != nil {
@ -409,6 +439,7 @@ func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
target.Endpoint = endpoint.String
target.Timeout = time.Duration(timeout.Int64)
target.InterruptOnError = interruptOnError.Bool
target.signingKey = signingKey
targets = append(targets, target)
}

View File

@ -11,7 +11,7 @@ import (
)
const (
TargetTable = "projections.targets1"
TargetTable = "projections.targets2"
TargetIDCol = "id"
TargetCreationDateCol = "creation_date"
TargetChangeDateCol = "change_date"
@ -23,6 +23,7 @@ const (
TargetEndpointCol = "endpoint"
TargetTimeoutCol = "timeout"
TargetInterruptOnErrorCol = "interrupt_on_error"
TargetSigningKey = "signing_key"
)
type targetProjection struct{}
@ -49,6 +50,7 @@ func (*targetProjection) Init() *old_handler.Check {
handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText),
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
handler.NewColumn(TargetSigningKey, handler.ColumnTypeJSONB, handler.Nullable()),
},
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
),
@ -105,6 +107,7 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S
handler.NewCol(TargetTargetType, e.TargetType),
handler.NewCol(TargetTimeoutCol, e.Timeout),
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
handler.NewCol(TargetSigningKey, e.SigningKey),
},
), nil
}
@ -134,6 +137,9 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler
if e.InterruptOnError != nil {
values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError))
}
if e.SigningKey != nil {
values = append(values, handler.NewCol(TargetSigningKey, e.SigningKey))
}
return handler.NewUpdateStatement(
e,
values,

View File

@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) {
testEvent(
target.AddedEventType,
target.AggregateType,
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }}`),
),
eventstore.GenericEventMapper[target.AddedEvent],
),
@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.targets1 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
expectedStmt: "INSERT INTO projections.targets2 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error, signing_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
expectedArgs: []interface{}{
"instance-id",
"ro-id",
@ -54,6 +54,7 @@ func TestTargetProjection_reduces(t *testing.T) {
domain.TargetTypeWebhook,
3 * time.Second,
true,
anyArg{},
},
},
},
@ -67,7 +68,7 @@ func TestTargetProjection_reduces(t *testing.T) {
testEvent(
target.ChangedEventType,
target.AggregateType,
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true, "signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }}`),
),
eventstore.GenericEventMapper[target.ChangedEvent],
),
@ -79,7 +80,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.targets1 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (instance_id = $9) AND (id = $10)",
expectedStmt: "UPDATE projections.targets2 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error, signing_key) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@ -89,6 +90,7 @@ func TestTargetProjection_reduces(t *testing.T) {
"https://example.com",
3 * time.Second,
true,
anyArg{},
"instance-id",
"agg-id",
},
@ -116,7 +118,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1) AND (id = $2)",
expectedStmt: "DELETE FROM projections.targets2 WHERE (instance_id = $1) AND (id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
@ -145,7 +147,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.targets2 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@ -29,10 +29,11 @@ type Queries struct {
client *database.DB
caches *Caches
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
idpConfigEncryption crypto.EncryptionAlgorithm
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
checkPermission domain.PermissionCheck
keyEncryptionAlgorithm crypto.EncryptionAlgorithm
idpConfigEncryption crypto.EncryptionAlgorithm
targetEncryptionAlgorithm crypto.EncryptionAlgorithm
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error)
checkPermission domain.PermissionCheck
DefaultLanguage language.Tag
mutex sync.Mutex
@ -52,7 +53,7 @@ func StartQueries(
cacheConnectors connector.Connectors,
projections projection.Config,
defaults sd.SystemDefaults,
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm crypto.EncryptionAlgorithm,
idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm, certEncryptionAlgorithm, targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
zitadelRoles []authz.RoleMapping,
sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error),
permissionCheck func(q *Queries) domain.PermissionCheck,
@ -70,6 +71,7 @@ func StartQueries(
zitadelRoles: zitadelRoles,
keyEncryptionAlgorithm: keyEncryptionAlgorithm,
idpConfigEncryption: idpConfigEncryption,
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
sessionTokenVerifier: sessionTokenVerifier,
multifactors: domain.MultifactorConfigs{
OTP: domain.OTPConfig{

View File

@ -9,6 +9,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/zerrors"
@ -59,6 +60,10 @@ var (
name: projection.TargetInterruptOnErrorCol,
table: targetTable,
}
TargetColumnSigningKey = Column{
name: projection.TargetSigningKey,
table: targetTable,
}
)
type Targets struct {
@ -78,6 +83,20 @@ type Target struct {
Endpoint string
Timeout time.Duration
InterruptOnError bool
signingKey *crypto.CryptoValue
SigningKey string
}
func (t *Target) decryptSigningKey(alg crypto.EncryptionAlgorithm) error {
if t.signingKey == nil {
return nil
}
keyValue, err := crypto.DecryptString(t.signingKey, alg)
if err != nil {
return zerrors.ThrowInternal(err, "QUERY-bxevy3YXwy", "Errors.Internal")
}
t.SigningKey = keyValue
return nil
}
type TargetSearchQueries struct {
@ -93,21 +112,37 @@ func (q *TargetSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query
}
func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQueries) (targets *Targets, err error) {
func (q *Queries) SearchTargets(ctx context.Context, queries *TargetSearchQueries) (*Targets, error) {
eq := sq.Eq{
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
query, scan := prepareTargetsQuery(ctx, q.client)
return genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan)
targets, err := genericRowsQueryWithState[*Targets](ctx, q.client, targetTable, combineToWhereStmt(query, queries.toQuery, eq), scan)
if err != nil {
return nil, err
}
for i := range targets.Targets {
if err := targets.Targets[i].decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
return nil, err
}
}
return targets, nil
}
func (q *Queries) GetTargetByID(ctx context.Context, id string) (target *Target, err error) {
func (q *Queries) GetTargetByID(ctx context.Context, id string) (*Target, error) {
eq := sq.Eq{
TargetColumnID.identifier(): id,
TargetColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
query, scan := prepareTargetQuery(ctx, q.client)
return genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan)
target, err := genericRowQuery[*Target](ctx, q.client, query.Where(eq), scan)
if err != nil {
return nil, err
}
if err := target.decryptSigningKey(q.targetEncryptionAlgorithm); err != nil {
return nil, err
}
return target, nil
}
func NewTargetNameSearchQuery(method TextComparison, value string) (SearchQuery, error) {
@ -129,6 +164,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu
TargetColumnTimeout.identifier(),
TargetColumnURL.identifier(),
TargetColumnInterruptOnError.identifier(),
TargetColumnSigningKey.identifier(),
countColumn.identifier(),
).From(targetTable.identifier()).
PlaceholderFormat(sq.Dollar),
@ -147,6 +183,7 @@ func prepareTargetsQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fu
&target.Timeout,
&target.Endpoint,
&target.InterruptOnError,
&target.signingKey,
&count,
)
if err != nil {
@ -179,6 +216,7 @@ func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fun
TargetColumnTimeout.identifier(),
TargetColumnURL.identifier(),
TargetColumnInterruptOnError.identifier(),
TargetColumnSigningKey.identifier(),
).From(targetTable.identifier()).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Target, error) {
@ -193,6 +231,7 @@ func prepareTargetQuery(context.Context, prepareDatabase) (sq.SelectBuilder, fun
&target.Timeout,
&target.Endpoint,
&target.InterruptOnError,
&target.signingKey,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {

View File

@ -9,22 +9,24 @@ import (
"testing"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
prepareTargetsStmt = `SELECT projections.targets1.id,` +
` projections.targets1.creation_date,` +
` projections.targets1.change_date,` +
` projections.targets1.resource_owner,` +
` projections.targets1.name,` +
` projections.targets1.target_type,` +
` projections.targets1.timeout,` +
` projections.targets1.endpoint,` +
` projections.targets1.interrupt_on_error,` +
prepareTargetsStmt = `SELECT projections.targets2.id,` +
` projections.targets2.creation_date,` +
` projections.targets2.change_date,` +
` projections.targets2.resource_owner,` +
` projections.targets2.name,` +
` projections.targets2.target_type,` +
` projections.targets2.timeout,` +
` projections.targets2.endpoint,` +
` projections.targets2.interrupt_on_error,` +
` projections.targets2.signing_key,` +
` COUNT(*) OVER ()` +
` FROM projections.targets1`
` FROM projections.targets2`
prepareTargetsCols = []string{
"id",
"creation_date",
@ -35,19 +37,21 @@ var (
"timeout",
"endpoint",
"interrupt_on_error",
"signing_key",
"count",
}
prepareTargetStmt = `SELECT projections.targets1.id,` +
` projections.targets1.creation_date,` +
` projections.targets1.change_date,` +
` projections.targets1.resource_owner,` +
` projections.targets1.name,` +
` projections.targets1.target_type,` +
` projections.targets1.timeout,` +
` projections.targets1.endpoint,` +
` projections.targets1.interrupt_on_error` +
` FROM projections.targets1`
prepareTargetStmt = `SELECT projections.targets2.id,` +
` projections.targets2.creation_date,` +
` projections.targets2.change_date,` +
` projections.targets2.resource_owner,` +
` projections.targets2.name,` +
` projections.targets2.target_type,` +
` projections.targets2.timeout,` +
` projections.targets2.endpoint,` +
` projections.targets2.interrupt_on_error,` +
` projections.targets2.signing_key` +
` FROM projections.targets2`
prepareTargetCols = []string{
"id",
"creation_date",
@ -58,6 +62,7 @@ var (
"timeout",
"endpoint",
"interrupt_on_error",
"signing_key",
}
)
@ -102,6 +107,12 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
},
),
@ -123,6 +134,12 @@ func Test_TargetPrepares(t *testing.T) {
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: true,
signingKey: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
},
},
@ -145,6 +162,12 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
{
"id-2",
@ -156,6 +179,12 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
false,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
{
"id-3",
@ -167,6 +196,12 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
false,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
},
),
@ -188,6 +223,12 @@ func Test_TargetPrepares(t *testing.T) {
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: true,
signingKey: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
{
ObjectDetails: domain.ObjectDetails{
@ -201,6 +242,12 @@ func Test_TargetPrepares(t *testing.T) {
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: false,
signingKey: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
{
ObjectDetails: domain.ObjectDetails{
@ -214,6 +261,12 @@ func Test_TargetPrepares(t *testing.T) {
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: false,
signingKey: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
},
},
@ -270,6 +323,12 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
),
},
@ -285,6 +344,12 @@ func Test_TargetPrepares(t *testing.T) {
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: true,
signingKey: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
},
},
{

View File

@ -31,9 +31,9 @@ WITH RECURSIVE
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
AND e.include = p.execution_id)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error, t.signing_key
FROM dissolved_execution_targets e
JOIN projections.targets1 t
JOIN projections.targets2 t
ON e.instance_id = t.instance_id
AND e.target_id = t.id
WHERE "include" = ''

View File

@ -38,9 +38,9 @@ WITH RECURSIVE
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
AND e.include = p.execution_id)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error, t.signing_key
FROM dissolved_execution_targets e
JOIN projections.targets1 t
JOIN projections.targets2 t
ON e.instance_id = t.instance_id
AND e.target_id = t.id
WHERE "include" = ''

View File

@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
@ -18,11 +19,12 @@ const (
type AddedEvent struct {
eventstore.BaseEvent `json:"-"`
Name string `json:"name"`
TargetType domain.TargetType `json:"targetType"`
Endpoint string `json:"endpoint"`
Timeout time.Duration `json:"timeout"`
InterruptOnError bool `json:"interruptOnError"`
Name string `json:"name"`
TargetType domain.TargetType `json:"targetType"`
Endpoint string `json:"endpoint"`
Timeout time.Duration `json:"timeout"`
InterruptOnError bool `json:"interruptOnError"`
SigningKey *crypto.CryptoValue `json:"signingKey"`
}
func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
@ -45,22 +47,24 @@ func NewAddedEvent(
endpoint string,
timeout time.Duration,
interruptOnError bool,
signingKey *crypto.CryptoValue,
) *AddedEvent {
return &AddedEvent{
*eventstore.NewBaseEventForPush(
ctx, aggregate, AddedEventType,
),
name, targetType, endpoint, timeout, interruptOnError}
name, targetType, endpoint, timeout, interruptOnError, signingKey}
}
type ChangedEvent struct {
eventstore.BaseEvent `json:"-"`
Name *string `json:"name,omitempty"`
TargetType *domain.TargetType `json:"targetType,omitempty"`
Endpoint *string `json:"endpoint,omitempty"`
Timeout *time.Duration `json:"timeout,omitempty"`
InterruptOnError *bool `json:"interruptOnError,omitempty"`
Name *string `json:"name,omitempty"`
TargetType *domain.TargetType `json:"targetType,omitempty"`
Endpoint *string `json:"endpoint,omitempty"`
Timeout *time.Duration `json:"timeout,omitempty"`
InterruptOnError *bool `json:"interruptOnError,omitempty"`
SigningKey *crypto.CryptoValue `json:"signingKey,omitempty"`
oldName string
}
@ -134,6 +138,12 @@ func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) {
}
}
func ChangeSigningKey(signingKey *crypto.CryptoValue) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.SigningKey = signingKey
}
}
type RemovedEvent struct {
eventstore.BaseEvent `json:"-"`

115
pkg/actions/signing.go Normal file
View 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
}

View File

@ -408,6 +408,12 @@ message CreateTargetRequest {
message CreateTargetResponse {
zitadel.resources.object.v3alpha.Details details = 1;
// Key used to sign and check payload sent to the target.
string signing_key = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message PatchTargetRequest {
@ -433,6 +439,12 @@ message PatchTargetRequest {
message PatchTargetResponse {
zitadel.resources.object.v3alpha.Details details = 1;
// Key used to sign and check payload sent to the target.
optional string signing_key = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message DeleteTargetRequest {

View File

@ -9,6 +9,7 @@ import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "google/protobuf/timestamp.proto";
import "zitadel/resources/object/v3alpha/object.proto";
@ -51,6 +52,11 @@ message Target {
message GetTarget {
zitadel.resources.object.v3alpha.Details details = 1;
Target config = 2;
string signing_key = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message PatchTarget {
@ -84,6 +90,21 @@ message PatchTarget {
max_length: 1000
}
];
// Regenerate the key used for signing and checking the payload sent to the target.
// Set the graceful period for the existing key. During that time, the previous
// signing key and the new one will be used to sign the request to allow you a smooth
// transition onf your API.
//
// Note that we currently only allow an immediate rotation ("0s") and will support
// longer expirations in the future.
optional google.protobuf.Duration expiration_signing_key = 7 [
(validate.rules).duration = {const: {seconds: 0, nanos: 0}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"0s\""
minimum: 0
maximum: 0
}
];
}