feat: http provider signing key addition (#10641)

# Which Problems Are Solved

HTTP Request to HTTP providers for Email or SMS are not signed.

# How the Problems Are Solved

Add a Signing Key to the HTTP Provider resources, which is then used to
generate a header to sign the payload.

# Additional Changes

Additional tests for query side of the SMTP provider.

# Additional Context

Closes #10067

---------

Co-authored-by: Marco A. <marco@zitadel.com>
(cherry picked from commit 8909b9a2a6)
This commit is contained in:
Stefan Benz
2025-09-08 13:00:04 +02:00
committed by Livio Spring
parent d2d94ea088
commit 1a7cd6e1af
36 changed files with 2113 additions and 132 deletions

View File

@@ -37,6 +37,7 @@ const (
SMSHTTPColumnSMSID = "sms_id"
SMSHTTPColumnInstanceID = "instance_id"
SMSHTTPColumnEndpoint = "endpoint"
SMSHTTPColumnSigningKey = "signing_key"
)
type smsConfigProjection struct{}
@@ -80,6 +81,7 @@ func (*smsConfigProjection) Init() *old_handler.Check {
handler.NewColumn(SMSHTTPColumnSMSID, handler.ColumnTypeText),
handler.NewColumn(SMSHTTPColumnInstanceID, handler.ColumnTypeText),
handler.NewColumn(SMSHTTPColumnEndpoint, handler.ColumnTypeText),
handler.NewColumn(SMSHTTPColumnSigningKey, handler.ColumnTypeJSONB, handler.Nullable()),
},
handler.NewPrimaryKey(SMSHTTPColumnInstanceID, SMSHTTPColumnSMSID),
smsHTTPTableSuffix,
@@ -286,6 +288,7 @@ func (p *smsConfigProjection) reduceSMSConfigHTTPAdded(event eventstore.Event) (
handler.NewCol(SMSHTTPColumnSMSID, e.ID),
handler.NewCol(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID),
handler.NewCol(SMSHTTPColumnEndpoint, e.Endpoint),
handler.NewCol(SMSHTTPColumnSigningKey, e.SigningKey),
},
handler.WithTableSuffix(smsHTTPTableSuffix),
),
@@ -306,21 +309,24 @@ func (p *smsConfigProjection) reduceSMSConfigHTTPChanged(event eventstore.Event)
if e.Description != nil {
columns = append(columns, handler.NewCol(SMSColumnDescription, *e.Description))
}
if len(columns) > 0 {
stmts = append(stmts, handler.AddUpdateStatement(
columns,
[]handler.Condition{
handler.NewCond(SMSColumnID, e.ID),
handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID),
},
))
}
stmts = append(stmts, handler.AddUpdateStatement(
columns,
[]handler.Condition{
handler.NewCond(SMSColumnID, e.ID),
handler.NewCond(SMSColumnInstanceID, e.Aggregate().InstanceID),
},
))
httpColumns := make([]handler.Column, 0)
if e.SigningKey != nil {
httpColumns = append(httpColumns, handler.NewCol(SMSHTTPColumnSigningKey, e.SigningKey))
}
if e.Endpoint != nil {
httpColumns = append(httpColumns, handler.NewCol(SMSHTTPColumnEndpoint, *e.Endpoint))
}
if len(httpColumns) > 0 {
stmts = append(stmts, handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol(SMSHTTPColumnEndpoint, *e.Endpoint),
},
httpColumns,
[]handler.Condition{
handler.NewCond(SMSHTTPColumnSMSID, e.ID),
handler.NewCond(SMSHTTPColumnInstanceID, e.Aggregate().InstanceID),

View File

@@ -302,7 +302,8 @@ func TestSMSProjection_reduces(t *testing.T) {
[]byte(`{
"id": "id",
"description": "description",
"endpoint": "endpoint"
"endpoint": "endpoint",
"signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }
}`),
), eventstore.GenericEventMapper[instance.SMSConfigHTTPAddedEvent]),
},
@@ -327,11 +328,12 @@ func TestSMSProjection_reduces(t *testing.T) {
},
},
{
expectedStmt: "INSERT INTO projections.sms_configs3_http (sms_id, instance_id, endpoint) VALUES ($1, $2, $3)",
expectedStmt: "INSERT INTO projections.sms_configs3_http (sms_id, instance_id, endpoint, signing_key) VALUES ($1, $2, $3, $4)",
expectedArgs: []interface{}{
"id",
"instance-id",
"endpoint",
anyArg{},
},
},
},
@@ -348,7 +350,8 @@ func TestSMSProjection_reduces(t *testing.T) {
[]byte(`{
"id": "id",
"endpoint": "endpoint",
"description": "description"
"description": "description",
"signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }
}`),
), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]),
},
@@ -369,8 +372,9 @@ func TestSMSProjection_reduces(t *testing.T) {
},
},
{
expectedStmt: "UPDATE projections.sms_configs3_http SET endpoint = $1 WHERE (sms_id = $2) AND (instance_id = $3)",
expectedStmt: "UPDATE projections.sms_configs3_http SET (signing_key, endpoint) = ($1, $2) WHERE (sms_id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
anyArg{},
"endpoint",
"id",
"instance-id",
@@ -452,6 +456,46 @@ func TestSMSProjection_reduces(t *testing.T) {
},
},
},
{
name: "instance reduceSMSConfigHTTPChanged, only signing key",
args: args{
event: getEvent(
testEvent(
instance.SMSConfigHTTPChangedEventType,
instance.AggregateType,
[]byte(`{
"id": "id",
"signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }
}`),
), eventstore.GenericEventMapper[instance.SMSConfigHTTPChangedEvent]),
},
reduce: (&smsConfigProjection{}).reduceSMSConfigHTTPChanged,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sms_configs3 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"id",
"instance-id",
},
},
{
expectedStmt: "UPDATE projections.sms_configs3_http SET signing_key = $1 WHERE (sms_id = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{
anyArg{},
"id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceSMSConfigTwilioActivated",
args: args{

View File

@@ -40,6 +40,7 @@ const (
SMTPConfigHTTPColumnInstanceID = "instance_id"
SMTPConfigHTTPColumnID = "id"
SMTPConfigHTTPColumnEndpoint = "endpoint"
SMTPConfigHTTPColumnSigningKey = "signing_key"
)
type smtpConfigProjection struct{}
@@ -86,6 +87,7 @@ func (*smtpConfigProjection) Init() *old_handler.Check {
handler.NewColumn(SMTPConfigHTTPColumnID, handler.ColumnTypeText),
handler.NewColumn(SMTPConfigHTTPColumnInstanceID, handler.ColumnTypeText),
handler.NewColumn(SMTPConfigHTTPColumnEndpoint, handler.ColumnTypeText),
handler.NewColumn(SMTPConfigHTTPColumnSigningKey, handler.ColumnTypeJSONB, handler.Nullable()),
},
handler.NewPrimaryKey(SMTPConfigHTTPColumnInstanceID, SMTPConfigHTTPColumnID),
smtpConfigHTTPTableSuffix,
@@ -211,6 +213,7 @@ func (p *smtpConfigProjection) reduceSMTPConfigHTTPAdded(event eventstore.Event)
handler.NewCol(SMTPConfigHTTPColumnInstanceID, e.Aggregate().InstanceID),
handler.NewCol(SMTPConfigHTTPColumnID, getSMTPConfigID(e.ID, e.Aggregate())),
handler.NewCol(SMTPConfigHTTPColumnEndpoint, e.Endpoint),
handler.NewCol(SMTPConfigHTTPColumnSigningKey, e.SigningKey),
},
handler.WithTableSuffix(smtpConfigHTTPTableSuffix),
),
@@ -231,20 +234,21 @@ func (p *smtpConfigProjection) reduceSMTPConfigHTTPChanged(event eventstore.Even
if e.Description != nil {
columns = append(columns, handler.NewCol(SMTPConfigColumnDescription, *e.Description))
}
if len(columns) > 0 {
stmts = append(stmts, handler.AddUpdateStatement(
columns,
[]handler.Condition{
handler.NewCond(SMTPConfigColumnID, getSMTPConfigID(e.ID, e.Aggregate())),
handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID),
},
))
}
stmts = append(stmts, handler.AddUpdateStatement(
columns,
[]handler.Condition{
handler.NewCond(SMTPConfigColumnID, getSMTPConfigID(e.ID, e.Aggregate())),
handler.NewCond(SMTPConfigColumnInstanceID, e.Aggregate().InstanceID),
},
))
smtpColumns := make([]handler.Column, 0, 1)
if e.Endpoint != nil {
smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigHTTPColumnEndpoint, *e.Endpoint))
}
if e.SigningKey != nil {
smtpColumns = append(smtpColumns, handler.NewCol(SMTPConfigHTTPColumnSigningKey, e.SigningKey))
}
if len(smtpColumns) > 0 {
stmts = append(stmts, handler.AddUpdateStatement(
smtpColumns,

View File

@@ -225,7 +225,8 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
"aggregate_id": "agg-id",
"id": "config-id",
"description": "test",
"endpoint": "endpoint"
"endpoint": "endpoint",
"signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }
}`,
),
), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]),
@@ -247,9 +248,10 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
},
},
{
expectedStmt: "UPDATE projections.smtp_configs5_http SET endpoint = $1 WHERE (id = $2) AND (instance_id = $3)",
expectedStmt: "UPDATE projections.smtp_configs5_http SET (endpoint, signing_key) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
"endpoint",
anyArg{},
"config-id",
"instance-id",
},
@@ -338,6 +340,49 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
},
},
},
}, {
name: "reduceSMTPConfigHTTPChanged, signing key",
args: args{
event: getEvent(
testEvent(
instance.SMTPConfigHTTPChangedEventType,
instance.AggregateType,
[]byte(`{
"instance_id": "instance-id",
"resource_owner": "ro-id",
"aggregate_id": "agg-id",
"id": "config-id",
"signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }
}`,
),
), eventstore.GenericEventMapper[instance.SMTPConfigHTTPChangedEvent]),
},
reduce: (&smtpConfigProjection{}).reduceSMTPConfigHTTPChanged,
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.smtp_configs5 SET (change_date, sequence) = ($1, $2) WHERE (id = $3) AND (instance_id = $4)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
"config-id",
"instance-id",
},
},
{
expectedStmt: "UPDATE projections.smtp_configs5_http SET signing_key = $1 WHERE (id = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{
anyArg{},
"config-id",
"instance-id",
},
},
},
},
},
},
{
name: "reduceSMTPConfigAdded (no id)",
@@ -481,7 +526,8 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
"id": "config-id",
"description": "test",
"senderAddress": "sender",
"endpoint": "endpoint"
"endpoint": "endpoint",
"signingKey": { "cryptoType": 0, "algorithm": "RSA-265", "keyId": "key-id" }
}`),
), eventstore.GenericEventMapper[instance.SMTPConfigHTTPAddedEvent]),
},
@@ -506,11 +552,12 @@ func TestSMTPConfigProjection_reduces(t *testing.T) {
},
},
{
expectedStmt: "INSERT INTO projections.smtp_configs5_http (instance_id, id, endpoint) VALUES ($1, $2, $3)",
expectedStmt: "INSERT INTO projections.smtp_configs5_http (instance_id, id, endpoint, signing_key) VALUES ($1, $2, $3, $4)",
expectedArgs: []interface{}{
"instance-id",
"config-id",
"endpoint",
anyArg{},
},
},
},