Merge commit from fork

* Add DenyLists parsing

* Remove unneeded returned error

* Plug global denylist into Command

* app creation: apply denylist to backchannel logout URI

* Inject denylist to backchannel logout worker

* webhook config: validate against blocked URLs

* Add notificationsWebhook denylist target

* command: Add SMTP endpoint validation against blocklist

* command: Add SMS endpoint validation against blocklist

* Validate webhook endpoint against denylist on channel notification

* Remove unused tests

* handle deprecated denylists

* remove unintended denylist entry in deprecated list

* use single http client

* fix tests

* update comments

* fixes

* cleanup

* address comments

* fix merge

---------

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Marco A.
2026-06-15 15:36:14 +02:00
committed by GitHub
co-authored by Livio Spring
parent d184e976fc
commit 8e82ec1cb9
97 changed files with 1834 additions and 285 deletions
+9 -23
View File
@@ -9,9 +9,7 @@ import (
"encoding/json"
"encoding/pem"
"io"
"net"
"net/http"
"net/url"
"sync"
"time"
@@ -22,7 +20,6 @@ import (
zhttp "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc/sign"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
target_domain "github.com/zitadel/zitadel/internal/execution/target"
"github.com/zitadel/zitadel/internal/repository/execution"
@@ -46,7 +43,7 @@ func CallTargets(
info ContextInfo,
alg crypto.EncryptionAlgorithm,
activeSigningKey GetActiveSigningWebKey,
deniedIPList []denylist.AddressChecker,
client *http.Client,
) (_ any, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -58,7 +55,7 @@ func CallTargets(
for _, target := range targets {
// call the type of target
resp, err := CallTarget(ctx, target, info, alg, signerOnce, encrypters, deniedIPList)
resp, err := CallTarget(ctx, target, info, alg, signerOnce, encrypters, client)
// handle error if interrupt is set
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "target", target.GetTargetID()).OnError(err).Error("error calling target")
if err != nil && target.IsInterruptOnError() {
@@ -86,7 +83,7 @@ func CallTarget(
alg crypto.EncryptionAlgorithm,
signerOnce sign.SignerFunc,
encrypters *sync.Map,
deniedIPList []denylist.AddressChecker,
client *http.Client,
) (res []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -96,16 +93,6 @@ func CallTarget(
return nil, zerrors.ThrowInternal(err, "EXEC-thiiCh5b", "Errors.Internal")
}
if target.GetEndpoint() != "" {
endpointURL, err := url.Parse(target.GetEndpoint())
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "EXEC-N5lu09", "Errors.Endpoint.Invalid")
}
if err := denylist.IsHostBlocked(deniedIPList, endpointURL, net.LookupIP); err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "EXEC-N5lu09", "Errors.Endpoint.Denied")
}
}
body, err := payload(ctx, info.GetHTTPRequestBody(), target, signerOnce, encrypters)
if err != nil {
return nil, err
@@ -114,13 +101,13 @@ func CallTarget(
switch target.GetTargetType() {
// get request, ignore response and return request and error for handling in list of targets
case target_domain.TargetTypeWebhook:
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey)
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey, client)
// get request, return response and error
case target_domain.TargetTypeCall:
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey)
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey, client)
case target_domain.TargetTypeAsync:
go func(ctx context.Context, target target_domain.Target, info []byte) {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, signingKey); err != nil {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, signingKey, client); err != nil {
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
}
}(context.WithoutCancel(ctx), target, body)
@@ -238,13 +225,13 @@ func publicKeyFromBytes(data []byte) (any, jose.KeyAlgorithm, error) {
}
// webhook call a webhook, ignore the response but return the errror
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) error {
_, err := Call(ctx, url, timeout, body, signingKey)
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string, client *http.Client) error {
_, err := Call(ctx, url, timeout, body, signingKey, client)
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, signingKey string) (_ []byte, err error) {
func Call(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string, client *http.Client) (_ []byte, err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, span := tracing.NewSpan(ctx)
defer func() {
@@ -261,7 +248,6 @@ func Call(ctx context.Context, url string, timeout time.Duration, body []byte, s
req.Header.Set(actions.SigningHeader, actions.ComputeSignatureHeader(time.Now(), body, signingKey))
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
return nil, err
+4 -6
View File
@@ -423,9 +423,7 @@ func Test_CallTarget(t *testing.T) {
}
func Test_CallTargets(t *testing.T) {
deniedLocalhost, err := denylist.NewHostChecker("127.0.0.1")
require.NoError(t, err)
deniedIPs := []denylist.AddressChecker{deniedLocalhost}
deniedIPs := []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.1")}
type args struct {
ctx context.Context
info *middleware.ContextInfoRequest
@@ -720,7 +718,7 @@ func checkRequest(t *testing.T, sent *http.Request, method string, checkExpected
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, signingKey)
return execution.Call(ctx, url, timeout, body, signingKey, http.DefaultClient)
}
}
@@ -734,7 +732,7 @@ func testCallTarget(ctx context.Context,
) func(string) ([]byte, error) {
return func(url string) (r []byte, err error) {
target.Endpoint = url
return execution.CallTarget(ctx, target, info, alg, signerOnce, encrypters, actionsDenyList)
return execution.CallTarget(ctx, target, info, alg, signerOnce, encrypters, &http.Client{Transport: denylist.NewHTTPTransport(actionsDenyList)})
}
}
@@ -751,7 +749,7 @@ func testCallTargets(ctx context.Context,
t.Endpoint = urls[i]
targets[i] = t
}
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, actionsDenyList)
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, &http.Client{Transport: denylist.NewHTTPTransport(actionsDenyList)})
}
}
+3 -1
View File
@@ -2,6 +2,7 @@ package execution
import (
"context"
"net/http"
"time"
"github.com/zitadel/zitadel/internal/crypto"
@@ -16,12 +17,13 @@ var (
func Register(
ctx context.Context,
workerConfig WorkerConfig,
httpClient *http.Client,
queue *queue.Queue,
targetEncAlg crypto.EncryptionAlgorithm,
activeSigningKey GetActiveSigningWebKey,
) {
queue.ShouldStart()
queue.AddWorkers(ctx, NewWorker(workerConfig, targetEncAlg, activeSigningKey, time.Now))
queue.AddWorkers(ctx, NewWorker(workerConfig, targetEncAlg, activeSigningKey, time.Now, httpClient))
}
func Start(ctx context.Context) {
+7 -3
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/riverqueue/river"
@@ -18,8 +19,9 @@ import (
type Worker struct {
river.WorkerDefaults[*exec_repo.Request]
config WorkerConfig
now NowFunc
config WorkerConfig
httpClient *http.Client
now NowFunc
targetEncAlg crypto.EncryptionAlgorithm
activeSigningKey GetActiveSigningWebKey
@@ -47,7 +49,7 @@ func (w *Worker) Work(ctx context.Context, job *river.Job[*exec_repo.Request]) e
return river.JobCancel(fmt.Errorf("unable to unmarshal targets because %w", err))
}
_, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args), w.targetEncAlg, w.activeSigningKey, w.config.DenyList)
_, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args), w.targetEncAlg, w.activeSigningKey, w.httpClient)
if err != nil {
// If there is an error returned from the targets, it means that the execution was interrupted
return river.JobCancel(fmt.Errorf("interruption during call of targets because %w", err))
@@ -70,9 +72,11 @@ func NewWorker(
targetEncAlg crypto.EncryptionAlgorithm,
activeSigningKey GetActiveSigningWebKey,
now NowFunc,
httpClient *http.Client,
) *Worker {
return &Worker{
config: config,
httpClient: httpClient,
now: now,
targetEncAlg: targetEncAlg,
activeSigningKey: activeSigningKey,
+1
View File
@@ -61,6 +61,7 @@ func newExecutionWorker(f fieldsWorker) *execution.Worker {
nil,
mockGetActiveSigningWebKey,
f.now,
http.DefaultClient,
)
}