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
+4 -4
View File
@@ -41,7 +41,7 @@ func (a *AddTarget) isValid(inputDenyList []denylist.AddressChecker, lookupFunc
return zerrors.ThrowInvalidArgument(err, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
}
if err := denylist.IsHostBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
if err := denylist.IsURLBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-NcJUKo", "Errors.Target.DeniedURL")
}
@@ -53,7 +53,7 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing")
}
if err := add.isValid(c.ActionsV2DenyList, c.IPLookupFunction); err != nil {
if err := add.isValid(c.denyList, c.ipLookupFunction); err != nil {
return time.Time{}, err
}
@@ -124,7 +124,7 @@ func (a *ChangeTarget) isValid(inputDenyList []denylist.AddressChecker, lookupFu
if err != nil || *a.Endpoint == "" {
return zerrors.ThrowInvalidArgument(err, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
}
if err := denylist.IsHostBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
if err := denylist.IsURLBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-jKbbu2", "Errors.Target.DeniedURL")
}
}
@@ -136,7 +136,7 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
if resourceOwner == "" {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing")
}
if err := change.isValid(c.ActionsV2DenyList, c.IPLookupFunction); err != nil {
if err := change.isValid(c.denyList, c.ipLookupFunction); err != nil {
return time.Time{}, err
}
existing, err := c.getTargetWriteModelByID(ctx, change.AggregateID, resourceOwner)
+5 -8
View File
@@ -8,7 +8,6 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
@@ -23,8 +22,7 @@ import (
)
func TestCommands_AddTarget(t *testing.T) {
localhostAddrChecker, err := denylist.NewHostChecker("localhost")
require.NoError(t, err)
localhostAddrChecker := denylist.NewHostChecker("localhost")
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
@@ -305,7 +303,7 @@ func TestCommands_AddTarget(t *testing.T) {
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
ActionsV2DenyList: tt.fields.denyList,
denyList: tt.fields.denyList,
}
_, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -322,8 +320,7 @@ func TestCommands_AddTarget(t *testing.T) {
}
func TestCommands_ChangeTarget(t *testing.T) {
localhostAddrChecker, err := denylist.NewHostChecker("localhost")
require.NoError(t, err)
localhostAddrChecker := denylist.NewHostChecker("localhost")
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
@@ -647,8 +644,8 @@ func TestCommands_ChangeTarget(t *testing.T) {
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
ActionsV2DenyList: tt.fields.denyList,
IPLookupFunction: tt.fields.lookupFunc,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.lookupFunc,
}
_, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
if tt.res.err == nil {
+10 -9
View File
@@ -106,9 +106,9 @@ type Commands struct {
// so the query and cache overhead can be completely eliminated.
milestonesCompleted sync.Map
loginPaths LoginPaths
ActionsV2DenyList []denylist.AddressChecker
IPLookupFunction internal_net.IPLookupFunc
loginPaths LoginPaths
ipLookupFunction internal_net.IPLookupFunc
denyList []denylist.AddressChecker
}
//go:generate mockgen -package command -destination ./mock_login_paths.go . LoginPaths
@@ -138,7 +138,7 @@ func StartCommands(
defaultAccessTokenLifetime, defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime time.Duration,
defaultSecretGenerators *SecretGenerators,
loginPaths LoginPaths,
actionsDeniedHostList []denylist.AddressChecker,
denyList []denylist.AddressChecker,
) (repo *Commands, err error) {
if externalDomain == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
@@ -162,6 +162,7 @@ func StartCommands(
if err != nil {
return nil, fmt.Errorf("caches: %w", err)
}
ipLookupFunction := net.LookupIP
repo = &Commands{
eventstore: es,
static: staticStore,
@@ -228,11 +229,11 @@ func StartCommands(
WithHyphen: defaults.Multifactors.RecoveryCodes.WithHyphen,
},
},
GenerateDomain: domain.NewGeneratedInstanceDomain,
caches: caches,
loginPaths: loginPaths,
ActionsV2DenyList: actionsDeniedHostList,
IPLookupFunction: net.LookupIP,
GenerateDomain: domain.NewGeneratedInstanceDomain,
caches: caches,
loginPaths: loginPaths,
ipLookupFunction: ipLookupFunction,
denyList: denyList,
}
if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil {
+2 -1
View File
@@ -95,11 +95,12 @@ func (c *Commands) GetProvider(ctx context.Context, idpID string, idpCallback st
return nil, err
}
if writeModel.IDPType != domain.IDPTypeSAML {
return writeModel.ToProvider(idpCallback, c.idpConfigEncryption)
return writeModel.ToProvider(idpCallback, c.idpConfigEncryption, c.httpClient)
}
return writeModel.ToSAMLProvider(
samlRootURL,
c.idpConfigEncryption,
c.httpClient,
func(ctx context.Context, intentID string) (*samlsp.TrackedRequest, error) {
intent, err := c.GetActiveIntent(ctx, intentID)
if err != nil {
+51 -18
View File
@@ -164,7 +164,7 @@ func (wm *OAuthIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -202,6 +202,7 @@ func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encry
func() providers.User {
return oauth.NewUserMapper(wm.IDAttribute)
},
httpClient,
opts...,
)
}
@@ -380,7 +381,7 @@ func (wm *OIDCIDPWriteModel) reduceOIDCConfigChangedEvent(e *idpconfig.OIDCConfi
}
}
func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -413,6 +414,7 @@ func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp
callbackURL,
wm.Scopes,
oidc.DefaultMapper,
httpClient,
opts...,
)
}
@@ -572,7 +574,7 @@ func (wm *JWTIDPWriteModel) reduceJWTConfigChangedEvent(e *idpconfig.JWTConfigCh
}
}
func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
opts := make([]jwt.ProviderOpts, 0)
if wm.IsCreationAllowed {
opts = append(opts, jwt.WithCreationAllowed())
@@ -594,6 +596,7 @@ func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encrypt
wm.HeaderName,
wm.Audience,
idpAlg,
httpClient,
opts...,
)
}
@@ -708,7 +711,7 @@ func (wm *AzureADIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -742,6 +745,7 @@ func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Enc
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -836,7 +840,7 @@ func (wm *GitHubIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -859,6 +863,7 @@ func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encr
secret,
callbackURL,
wm.Scopes,
httpClient,
oauthOpts...,
)
}
@@ -980,7 +985,7 @@ func (wm *GitHubEnterpriseIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -1007,6 +1012,7 @@ func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg c
wm.TokenEndpoint,
wm.UserEndpoint,
wm.Scopes,
httpClient,
oauthOpts...,
)
}
@@ -1102,7 +1108,7 @@ func (wm *GitLabIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -1125,6 +1131,7 @@ func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encr
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1228,7 +1235,7 @@ func (wm *GitLabSelfHostedIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -1253,6 +1260,7 @@ func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg c
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1350,7 +1358,7 @@ func (wm *GoogleIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state)
rp.DefaultErrorHandler(w, r, errorType, errorDesc, state)
@@ -1378,6 +1386,7 @@ func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encr
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1552,7 +1561,7 @@ func (wm *LDAPIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
password, err := crypto.DecryptString(wm.BindPassword, idpAlg)
if err != nil {
return nil, err
@@ -1731,7 +1740,7 @@ func (wm *AppleIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
privateKey, err := crypto.Decrypt(wm.PrivateKey, idpAlg)
if err != nil {
return nil, err
@@ -1756,6 +1765,7 @@ func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encry
callbackURL,
privateKey,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1902,7 +1912,13 @@ func (wm *SAMLIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) {
func (wm *SAMLIDPWriteModel) ToProvider(
callbackURL string,
idpAlg crypto.EncryptionAlgorithm,
httpClient *http.Client,
getRequest requesttracker.GetRequest,
addRequest requesttracker.AddRequest,
) (providers.Provider, error) {
key, err := crypto.Decrypt(wm.Key, idpAlg)
if err != nil {
return nil, err
@@ -1949,6 +1965,7 @@ func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp
wm.Metadata,
wm.Certificate,
key,
httpClient,
opts...,
)
}
@@ -2202,13 +2219,13 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
type IDP interface {
eventstore.QueryReducer
ToProvider(string, crypto.EncryptionAlgorithm) (providers.Provider, error)
ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error)
GetProviderOptions() idp.Options
}
type SAMLIDP interface {
eventstore.QueryReducer
ToProvider(string, crypto.EncryptionAlgorithm, requesttracker.GetRequest, requesttracker.AddRequest) (providers.Provider, error)
ToProvider(string, crypto.EncryptionAlgorithm, *http.Client, requesttracker.GetRequest, requesttracker.AddRequest) (providers.Provider, error)
GetProviderOptions() idp.Options
}
@@ -2318,11 +2335,15 @@ func (wm *AllIDPWriteModel) AppendEvents(events ...eventstore.Event) {
wm.samlModel.AppendEvents(events...)
}
func (wm *AllIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *AllIDPWriteModel) ToProvider(
callbackURL string,
idpAlg crypto.EncryptionAlgorithm,
httpClient *http.Client,
) (providers.Provider, error) {
if wm.model == nil {
return nil, zerrors.ThrowInternal(nil, "COMMAND-afvf0gc9sa", "ErrorsIDPConfig.NotExisting")
}
return wm.model.ToProvider(callbackURL, idpAlg)
return wm.model.ToProvider(callbackURL, idpAlg, httpClient)
}
func (wm *AllIDPWriteModel) GetProviderOptions() idp.Options {
@@ -2332,11 +2353,23 @@ func (wm *AllIDPWriteModel) GetProviderOptions() idp.Options {
return wm.samlModel.GetProviderOptions()
}
func (wm *AllIDPWriteModel) ToSAMLProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) {
func (wm *AllIDPWriteModel) ToSAMLProvider(
callbackURL string,
idpAlg crypto.EncryptionAlgorithm,
httpClient *http.Client,
getRequest requesttracker.GetRequest,
addRequest requesttracker.AddRequest,
) (providers.Provider, error) {
if wm.samlModel == nil {
return nil, zerrors.ThrowInternal(nil, "COMMAND-csi30hdscv", "ErrorsIDPConfig.NotExisting")
}
return wm.samlModel.ToProvider(callbackURL, idpAlg, getRequest, addRequest)
return wm.samlModel.ToProvider(
callbackURL,
idpAlg,
httpClient,
getRequest,
addRequest,
)
}
type ZitadelIDPWriteModel struct {
+2 -2
View File
@@ -377,7 +377,7 @@ func TestOAuthIDPWriteModel_ToProvider_WithPKCE(t *testing.T) {
UsePKCE: true,
}
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{})
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{}, http.DefaultClient)
require.NoError(t, err)
assertProviderUsesPKCE(t, provider)
@@ -417,7 +417,7 @@ func TestOIDCIDPWriteModel_ToProvider_WithPKCE(t *testing.T) {
UsePKCE: true,
}
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{})
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{}, http.DefaultClient)
require.NoError(t, err)
require.True(t, discoveryRequested.Load(), "expected OIDC discovery to be requested")
+28 -2
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"net/url"
"strings"
"time"
@@ -9,6 +10,7 @@ import (
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
project_repo "github.com/zitadel/zitadel/internal/repository/project"
@@ -207,6 +209,12 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
if err != nil {
return nil, err
}
backchannelLogoutURI, err := c.validateBackchannelLogoutURI(oidcApp)
if err != nil {
return nil, err
}
events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx,
projectAgg,
gu.Value(oidcApp.OIDCVersion),
@@ -227,7 +235,7 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
gu.Value(oidcApp.ClockSkew),
trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins),
gu.Value(oidcApp.SkipNativeAppSuccessPage),
strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI)),
backchannelLogoutURI,
gu.Value(oidcApp.LoginVersion),
strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)),
))
@@ -252,6 +260,20 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
return result, nil
}
func (c *Commands) validateBackchannelLogoutURI(oidcApp *domain.OIDCApp) (string, error) {
backchannelLogoutURL := strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI))
if backchannelLogoutURL != "" {
url, err := url.Parse(backchannelLogoutURL)
if err != nil {
return "", zerrors.ThrowInvalidArgument(err, "PROJECT-iZL2Ac", "Errors.Project.App.Invalid.BackchannelLogoutURL")
}
if err := denylist.IsURLBlocked(c.denyList, url, c.ipLookupFunction); err != nil {
return "", zerrors.ThrowInvalidArgument(err, "PROJECT-msNebo", "Errors.Project.App.Blocked.BackchannelLogoutURL")
}
}
return backchannelLogoutURL, nil
}
func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) {
if !oidc.IsValid() || oidc.AppID == "" || oidc.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5m9fs", "Errors.Project.App.OIDCConfigInvalid")
@@ -277,7 +299,11 @@ func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCA
projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel)
var backChannelLogout, loginBaseURI *string
if oidc.BackChannelLogoutURI != nil {
backChannelLogout = gu.Ptr(strings.TrimSpace(*oidc.BackChannelLogoutURI))
bcl, err := c.validateBackchannelLogoutURI(oidc)
if err != nil {
return nil, err
}
backChannelLogout = gu.Ptr(bcl)
}
if oidc.LoginBaseURI != nil {
@@ -2,6 +2,7 @@ package command
import (
"context"
"net"
"testing"
"time"
@@ -10,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -407,6 +409,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
denyList []denylist.AddressChecker
}
type args struct {
ctx context.Context
@@ -816,6 +819,83 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
},
},
},
{
name: "backchannel logout URI invalid, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
BackChannelLogoutURI: gu.Ptr("http://example.com\x00"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "backchannel logout URI on denylist, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
BackChannelLogoutURI: gu.Ptr("https://blocked.example.com/logout"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -828,6 +908,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
ClientSecret: emptyConfig,
},
checkPermission: newMockPermissionCheckAllowed(),
ipLookupFunction: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("1.2.3.4")}, nil
},
denyList: tt.fields.denyList,
}
c.setMilestonesCompletedForTest("instanceID")
got, err := c.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
@@ -848,6 +932,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
denyList []denylist.AddressChecker
}
type args struct {
ctx context.Context
@@ -1195,13 +1280,138 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
},
},
},
{
name: "backchannel logout URI invalid, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewOIDCConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
domain.OIDCVersionV1,
"app1",
"client1",
"",
nil,
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeNone,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
expectFilter(),
),
},
args: args{
ctx: context.Background(),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
BackChannelLogoutURI: gu.Ptr("http://example.com\x00"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "backchannel logout URI on denylist, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewOIDCConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
domain.OIDCVersionV1,
"app1",
"client1",
"",
nil,
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeNone,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
expectFilter(),
),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
},
args: args{
ctx: context.Background(),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
BackChannelLogoutURI: gu.Ptr("https://blocked.example.com/logout"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// t.Parallel()
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: newMockPermissionCheckAllowed(),
ipLookupFunction: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("1.2.3.4")}, nil
},
denyList: tt.fields.denyList,
}
got, err := r.UpdateOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
if tt.res.err == nil {
+12
View File
@@ -170,6 +170,11 @@ func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (er
return err
}
}
if err := c.validateNotificationWebhookEndpoint(config.Endpoint); err != nil {
return err
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return err
@@ -217,6 +222,13 @@ func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTT
if config.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing")
}
if config.Endpoint != nil {
if err := c.validateNotificationWebhookEndpoint(*config.Endpoint); err != nil {
return err
}
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return err
+66
View File
@@ -3,6 +3,7 @@ package command
import (
"context"
"errors"
"net"
"testing"
"time"
@@ -11,6 +12,7 @@ import (
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
@@ -312,6 +314,8 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
alg crypto.EncryptionAlgorithm
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
ctx context.Context
@@ -366,6 +370,9 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
@@ -381,6 +388,30 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
},
},
},
{
name: "add sms config http, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
http: &AddSMSHTTP{
ResourceOwner: "INSTANCE",
Description: "description",
Endpoint: "https://blocked.example.com/hook",
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -390,6 +421,8 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
smsEncryption: tt.fields.alg,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.AddSMSConfigHTTP(tt.args.ctx, tt.args.http)
if tt.res.err == nil {
@@ -410,6 +443,8 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
eventstore func(*testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
ctx context.Context
@@ -499,6 +534,9 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
),
),
),
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
@@ -552,6 +590,9 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
@@ -569,6 +610,29 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
},
},
},
{
name: "change sms config, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
http: &ChangeSMSHTTP{
ResourceOwner: "INSTANCE",
ID: "providerid",
Endpoint: gu.Ptr("https://blocked.example.com/hook"),
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -576,6 +640,8 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.ChangeSMSConfigHTTP(tt.args.ctx, tt.args.http)
if tt.res.err == nil {
+24
View File
@@ -3,11 +3,13 @@ package command
import (
"context"
"net"
"net/url"
"strings"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
@@ -304,6 +306,10 @@ func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigH
}
}
if err := c.validateNotificationWebhookEndpoint(config.Endpoint); err != nil {
return err
}
smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, "")
if err != nil {
return err
@@ -349,6 +355,10 @@ func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPC
return zerrors.ThrowInvalidArgument(nil, "COMMAND-2MHkV8ObWo", "Errors.IDMissing")
}
if err := c.validateNotificationWebhookEndpoint(config.Endpoint); err != nil {
return err
}
smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, "")
if err != nil {
return err
@@ -698,6 +708,20 @@ func (c *Commands) prepareAddAndActivateSMTPConfig(a *instance.Aggregate, descri
}
}
func (c *Commands) validateNotificationWebhookEndpoint(endpoint string) error {
if endpoint == "" {
return nil
}
uri, err := url.Parse(endpoint)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-Sz84nFo3aK", "Errors.Invalid.Argument")
}
if err := denylist.IsURLBlocked(c.denyList, uri, c.ipLookupFunction); err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-Uf0294nKls", "Errors.NotificationProvider.Blocked.Endpoint")
}
return nil
}
func getSMTPConfigWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, id, domain string) (_ *IAMSMTPConfigWriteModel, err error) {
writeModel := NewIAMSMTPConfigWriteModel(authz.GetInstance(ctx).InstanceID(), id, domain)
events, err := filter(ctx, writeModel.Query())
+127
View File
@@ -3,6 +3,7 @@ package command
import (
"context"
"errors"
"net"
"testing"
"time"
@@ -11,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
@@ -1335,6 +1337,8 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
idGenerator id.Generator
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
http *AddSMTPConfigHTTP
@@ -1387,6 +1391,9 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &AddSMTPConfigHTTP{
@@ -1401,6 +1408,29 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
},
},
},
{
name: "add smtp config, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &AddSMTPConfigHTTP{
ResourceOwner: "INSTANCE",
Description: "test",
Endpoint: "https://blocked.example.com/hook",
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1409,6 +1439,8 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.AddSMTPConfigHTTP(context.Background(), tt.args.http)
if tt.res.err == nil {
@@ -1430,6 +1462,8 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
eventstore func(t *testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
http *ChangeSMTPConfigHTTP
@@ -1479,6 +1513,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
eventstore: expectEventstore(
expectFilter(),
),
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
@@ -1516,6 +1553,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
),
),
),
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
@@ -1569,6 +1609,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
@@ -1585,6 +1628,28 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
},
},
},
{
name: "change smtp config, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
ResourceOwner: "INSTANCE",
ID: "configid",
Endpoint: "https://blocked.example.com/hook",
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1592,6 +1657,8 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.ChangeSMTPConfigHTTP(context.Background(), tt.args.http)
if tt.res.err == nil {
@@ -1607,6 +1674,66 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
}
}
func TestCommands_validateNotificationWebhookEndpoint(t *testing.T) {
t.Parallel()
blockedLookup := func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
}
wantBlockedErr := zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", "")
tests := []struct {
name string
denyList []denylist.AddressChecker
endpoint string
wantErr error
}{
{
name: "empty endpoint skips denylist check",
endpoint: "",
},
{
name: "endpoint not on denylist, ok",
endpoint: "https://external.example.com/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
},
{
name: "no denylist configured, ok",
endpoint: "https://anything.example.com/hook",
},
{
name: "endpoint domain on denylist, blocked",
endpoint: "https://blocked.example.com/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
wantErr: wantBlockedErr,
},
{
name: "endpoint resolves to blocked IP range, blocked",
endpoint: "https://internal.host/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("10.0.0.0/8")},
wantErr: wantBlockedErr,
},
{
name: "localhost IP directly in URL is blocked",
endpoint: "http://127.0.0.1/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.0/8")},
wantErr: wantBlockedErr,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
denyList: tt.denyList,
ipLookupFunction: blockedLookup,
}
err := r.validateNotificationWebhookEndpoint(tt.endpoint)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCommandSide_ActivateSMTPConfig(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore