mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-31 19:42:15 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user