mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +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:
@@ -77,6 +77,7 @@ LogStore:
|
||||
Enabled: true
|
||||
|
||||
Executions:
|
||||
# Deprecated: use HTTPClient.DenyList
|
||||
DenyList: # ZITADEL_EXECUTIONS_DENYLIST (comma separated list)
|
||||
|
||||
Projections:
|
||||
@@ -129,4 +130,7 @@ OIDC:
|
||||
DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
|
||||
|
||||
SAML:
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
|
||||
|
||||
HTTPClient:
|
||||
DenyList:
|
||||
+35
-10
@@ -598,12 +598,9 @@ Executions:
|
||||
MaxTtl: 5m # ZITADEL_EXECUTIONS_MAXTTL
|
||||
# List of domains and IPs that are not valid execution target's endpoints
|
||||
# Wildcard sub domains are currently unsupported
|
||||
|
||||
# Deprecated: use HTTPClient.DenyList instead. If both are set, this list will be merged into the HTTPClient.DenyList.
|
||||
DenyList: # ZITADEL_EXECUTIONS_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "127.0.0.0/8"
|
||||
- "::1"
|
||||
- "0.0.0.0"
|
||||
- "::"
|
||||
|
||||
Auth:
|
||||
# See Projections.BulkLimit
|
||||
@@ -1033,12 +1030,8 @@ SystemDefaults:
|
||||
Actions:
|
||||
HTTP:
|
||||
# Wildcard sub domains are currently unsupported
|
||||
# Deprecated: use HTTPClient.DenyList instead. If both are set, this list will be merged into the HTTPClient.DenyList.
|
||||
DenyList: # ZITADEL_ACTIONS_HTTP_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "127.0.0.0/8"
|
||||
- "::1"
|
||||
- "0.0.0.0"
|
||||
- "::"
|
||||
|
||||
LogStore:
|
||||
Access:
|
||||
@@ -1072,6 +1065,38 @@ Eventstore:
|
||||
# Maximum amount of push retries in case of primary key violation on the sequence
|
||||
MaxRetries: 5 #ZITADEL_EVENTSTORE_MAXRETRIES
|
||||
|
||||
HTTPClient:
|
||||
# Sets the limit of the response body size in bytes, which will be processed by the HTTP client.
|
||||
# If the response body exceeds this limit, the client will return an error.
|
||||
# The default value is 20MB.
|
||||
MaxBodySize: 20971520 # ZITADEL_HTTPCLIENT_MAXBODYSIZE
|
||||
# Sets the timeout for the HTTP client.
|
||||
Timeout: 30s # ZITADEL_HTTPCLIENT_TIMEOUT
|
||||
# Sets the number of allowed redirects for the HTTP client.
|
||||
MaxRedirects: 5 # ZITADEL_HTTPCLIENT_MAXREDIRECTS
|
||||
# Specifies if the HTTP client should follow redirects from HTTPS to HTTP.
|
||||
AllowHTTPSDowngrade: false # ZITADEL_HTTPCLIENT_ALLOWHTTPSDOWNGRADE
|
||||
# Specifies the list of IP addresses or CIDR ranges that are not allowed to be called by the HTTP client.
|
||||
# Note that hosts / domains are matched best-case and wildcards are not supported.
|
||||
# CRITICAL SECURITY NOTE: Domain rules (like "localhost") protect application-layer string matches.
|
||||
# Underneath, the network dialer relies exclusively on the CIDR ranges below to block resolved IPs and prevent DNS Rebinding.
|
||||
# Do not remove the CIDR subnets even if a domain mapping exists.
|
||||
DenyList: # ZITADEL_HTTPCLIENT_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "0.0.0.0/8" # Unspecified IPv4 address range / Local network routing trick protection
|
||||
- "10.0.0.0/8" # Class A private networks
|
||||
- "100.64.0.0/10" # Carrier-Grade NAT space
|
||||
- "127.0.0.0/8" # Loopback addresses (IPv4)
|
||||
- "169.254.0.0/16" # Link-local subnets (Covers Cloud Metadata Endpoints like AWS/GCP 169.254.169.254)
|
||||
- "172.16.0.0/12" # Class B private networks
|
||||
- "192.168.0.0/16" # Class C private networks
|
||||
- "198.18.0.0/15" # Benchmark/inter-network communication testing
|
||||
- "::/128" # Unspecified IPv6 address
|
||||
- "::1/128" # IPv6 loopback address
|
||||
- "::ffff:0:0/96" # IPv4-mapped IPv6 addresses (Prevents IPv6 encapsulation bypasses)
|
||||
- "fc00::/7" # Unique local addresses / IPv6 equivalent to private IPv4 subnets
|
||||
- "fe80::/10" # Link-local addresses / IPv6 equivalent to cloud metadata subnets
|
||||
|
||||
# The DefaultInstance section defines the default values for each new virtual instance that is created.
|
||||
# Check out https://zitadel.com/docs/concepts/structure/instance#multiple-virtual-instances for more information about virtual instances.
|
||||
# For the initial setup, the default values are used to create the first instance.
|
||||
|
||||
@@ -211,3 +211,35 @@ FirstInstance:
|
||||
|
||||
Log:
|
||||
Level: info
|
||||
|
||||
HTTPClient:
|
||||
# Sets the limit of the response body size in bytes, which will be processed by the HTTP client.
|
||||
# If the response body exceeds this limit, the client will return an error.
|
||||
# The default value is 20MB.
|
||||
MaxBodySize: 20971520 # ZITADEL_HTTPCLIENT_MAXBODYSIZE
|
||||
# Sets the timeout for the HTTP client.
|
||||
Timeout: 30s # ZITADEL_HTTPCLIENT_TIMEOUT
|
||||
# Sets the number of allowed redirects for the HTTP client.
|
||||
MaxRedirects: 5 # ZITADEL_HTTPCLIENT_MAXREDIRECTS
|
||||
# Specifies if the HTTP client should follow redirects from HTTPS to HTTP.
|
||||
AllowHTTPSDowngrade: false # ZITADEL_HTTPCLIENT_ALLOWHTTPSDOWNGRADE
|
||||
# Specifies the list of IP addresses or CIDR ranges that are not allowed to be called by the HTTP client.
|
||||
# Note that hosts / domains are matched best-case and wildcards are not supported.
|
||||
# CRITICAL SECURITY NOTE: Domain rules (like "localhost") protect application-layer string matches.
|
||||
# Underneath, the network dialer relies exclusively on the CIDR ranges below to block resolved IPs and prevent DNS Rebinding.
|
||||
# Do not remove the CIDR subnets even if a domain mapping exists.
|
||||
DenyList: # ZITADEL_HTTPCLIENT_DENYLIST (comma separated list)
|
||||
- localhost
|
||||
- "0.0.0.0/8" # Unspecified IPv4 address range / Local network routing trick protection
|
||||
- "10.0.0.0/8" # Class A private networks
|
||||
- "100.64.0.0/10" # Carrier-Grade NAT space
|
||||
- "127.0.0.0/8" # Loopback addresses (IPv4)
|
||||
- "169.254.0.0/16" # Link-local subnets (Covers Cloud Metadata Endpoints like AWS/GCP 169.254.169.254)
|
||||
- "172.16.0.0/12" # Class B private networks
|
||||
- "192.168.0.0/16" # Class C private networks
|
||||
- "198.18.0.0/15" # Benchmark/inter-network communication testing
|
||||
- "::/128" # Unspecified IPv6 address
|
||||
- "::1/128" # IPv6 loopback address
|
||||
- "::ffff:0:0/96" # IPv4-mapped IPv6 addresses (Prevents IPv6 encapsulation bypasses)
|
||||
- "fc00::/7" # Unique local addresses / IPv6 equivalent to private IPv4 subnets
|
||||
- "fe80::/10" # Link-local addresses / IPv6 equivalent to cloud metadata subnets
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -22,6 +21,7 @@ import (
|
||||
admin_handler "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/handler"
|
||||
admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
crypto_db "github.com/zitadel/zitadel/internal/crypto/database"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||
@@ -109,6 +108,7 @@ type ProjectionsConfig struct {
|
||||
WebAuthNName string
|
||||
DefaultInstance command.InstanceSetup
|
||||
AssetStorage static_config.AssetStorageConfig
|
||||
HTTPClient http.ClientConfig
|
||||
}
|
||||
|
||||
func migrateProjectionsFlags(cmd *cobra.Command) {
|
||||
@@ -187,6 +187,9 @@ func projections(
|
||||
DisplayName: config.WebAuthNName,
|
||||
ExternalSecure: config.ExternalSecure,
|
||||
}
|
||||
|
||||
httpClient := config.HTTPClient.NewClient()
|
||||
|
||||
commands, err := command.StartCommands(ctx,
|
||||
es,
|
||||
cacheConnectors,
|
||||
@@ -206,7 +209,7 @@ func projections(
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
httpClient,
|
||||
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
|
||||
},
|
||||
@@ -216,7 +219,7 @@ func projections(
|
||||
config.OIDC.DefaultRefreshTokenIdleExpiration,
|
||||
config.DefaultInstance.SecretGenerators,
|
||||
config.Login.DefaultPaths,
|
||||
[]denylist.AddressChecker{},
|
||||
config.HTTPClient.DenyList,
|
||||
)
|
||||
logging.OnError(ctx, err).Fatal("unable to start commands")
|
||||
|
||||
@@ -246,6 +249,7 @@ func projections(
|
||||
keys.SMTP,
|
||||
keys.SMS,
|
||||
nil,
|
||||
httpClient,
|
||||
)
|
||||
|
||||
config.Auth.Spooler.Client = client
|
||||
|
||||
+5
-2
@@ -3,6 +3,7 @@ package setup
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -47,6 +48,8 @@ type FirstInstance struct {
|
||||
externalPort uint16
|
||||
domain string
|
||||
defaultPaths *login.DefaultPaths
|
||||
httpClient *http.Client
|
||||
denylist []denylist.AddressChecker
|
||||
}
|
||||
|
||||
func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
@@ -89,7 +92,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
||||
nil,
|
||||
nil,
|
||||
oidcEncryption,
|
||||
nil,
|
||||
mig.httpClient,
|
||||
nil,
|
||||
nil,
|
||||
0,
|
||||
@@ -97,7 +100,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
|
||||
0,
|
||||
nil,
|
||||
mig.defaultPaths,
|
||||
[]denylist.AddressChecker{},
|
||||
mig.denylist,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/zitadel/zitadel/cmd/hooks"
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/cache/connector"
|
||||
@@ -64,6 +65,7 @@ type Config struct {
|
||||
WebAuthNName string
|
||||
Telemetry *handlers.TelemetryPusherConfig
|
||||
SystemAPIUsers map[string]*authz.SystemAPIUser
|
||||
HTTPClient http.ClientConfig
|
||||
}
|
||||
|
||||
type InitProjections struct {
|
||||
@@ -114,6 +116,8 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu
|
||||
|
||||
id.Configure(config.Machine)
|
||||
|
||||
config.HTTPClient.MergeDeprecatedDenylists(nil, config.Executions.DenyList)
|
||||
|
||||
// Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API.
|
||||
config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/cache/connector"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
@@ -61,7 +60,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
|
||||
0,
|
||||
nil,
|
||||
&login.DefaultPaths{},
|
||||
[]denylist.AddressChecker{},
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
+9
-3
@@ -170,6 +170,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
MaxRetries: config.Eventstore.MaxRetries,
|
||||
}))
|
||||
|
||||
httpClient := config.HTTPClient.NewClient()
|
||||
|
||||
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient.DB}
|
||||
steps.s2AssetsTable = &AssetTable{dbClient: dbClient.DB}
|
||||
|
||||
@@ -187,6 +189,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.FirstInstance.externalSecure = config.ExternalSecure
|
||||
steps.FirstInstance.externalPort = config.ExternalPort
|
||||
steps.FirstInstance.defaultPaths = config.Login.DefaultPaths
|
||||
steps.FirstInstance.httpClient = httpClient
|
||||
steps.FirstInstance.denylist = config.HTTPClient.DenyList
|
||||
|
||||
steps.s5LastFailed = &LastFailed{dbClient: dbClient.DB}
|
||||
steps.s6OwnerRemoveColumns = &OwnerRemoveColumns{dbClient: dbClient.DB}
|
||||
@@ -315,7 +319,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
}
|
||||
}
|
||||
|
||||
commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config)
|
||||
commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config, httpClient)
|
||||
steps.s59SetupWebkeys = &SetupWebkeys{eventstore: eventstoreClient, commands: commands}
|
||||
|
||||
repeatableSteps := []migration.RepeatableMigration{
|
||||
@@ -454,6 +458,7 @@ func startCommandsQueries(
|
||||
dbClient *database.DB,
|
||||
masterKey string,
|
||||
config *Config,
|
||||
httpClient *http.Client,
|
||||
) (
|
||||
*command.Commands,
|
||||
*query.Queries,
|
||||
@@ -572,7 +577,7 @@ func startCommandsQueries(
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
httpClient,
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
config.OIDC.DefaultAccessTokenLifetime,
|
||||
@@ -580,7 +585,7 @@ func startCommandsQueries(
|
||||
config.OIDC.DefaultRefreshTokenIdleExpiration,
|
||||
config.DefaultInstance.SecretGenerators,
|
||||
config.Login.DefaultPaths,
|
||||
config.Executions.DenyList,
|
||||
config.HTTPClient.DenyList,
|
||||
)
|
||||
logging.OnError(ctx, err).Fatal("unable to start commands")
|
||||
|
||||
@@ -610,6 +615,7 @@ func startCommandsQueries(
|
||||
keys.SMTP,
|
||||
keys.SMS,
|
||||
q,
|
||||
httpClient,
|
||||
)
|
||||
|
||||
return commands, queries, adminView, authView
|
||||
|
||||
+6
-1
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/api/saml"
|
||||
@@ -87,6 +88,7 @@ type Config struct {
|
||||
Quotas *QuotasConfig
|
||||
Telemetry *handlers.TelemetryPusherConfig
|
||||
ServicePing *serviceping.Config
|
||||
HTTPClient *http.ClientConfig
|
||||
}
|
||||
|
||||
type QuotasConfig struct {
|
||||
@@ -121,9 +123,12 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu
|
||||
}
|
||||
|
||||
id.Configure(config.Machine)
|
||||
|
||||
var actionsDenylist []denylist.AddressChecker
|
||||
if config.Actions != nil {
|
||||
actions.SetHTTPConfig(&config.Actions.HTTP)
|
||||
actionsDenylist = config.Actions.HTTP.DenyList
|
||||
}
|
||||
config.HTTPClient.MergeDeprecatedDenylists(actionsDenylist, config.Executions.DenyList)
|
||||
|
||||
err = config.SystemDefaults.Validate()
|
||||
if err != nil {
|
||||
|
||||
+27
-4
@@ -272,6 +272,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
|
||||
new_domain.SetWebAuthNConfig(webAuthNConfig)
|
||||
|
||||
httpClient := config.HTTPClient.NewClient()
|
||||
|
||||
commands, err := command.StartCommands(ctx,
|
||||
eventstoreClient,
|
||||
cacheConnectors,
|
||||
@@ -291,7 +293,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
httpClient,
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
config.OIDC.DefaultAccessTokenLifetime,
|
||||
@@ -299,7 +301,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
config.OIDC.DefaultRefreshTokenIdleExpiration,
|
||||
config.DefaultInstance.SecretGenerators,
|
||||
config.Login.DefaultPaths,
|
||||
config.Executions.DenyList,
|
||||
config.HTTPClient.DenyList,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start commands: %w", err)
|
||||
@@ -345,12 +347,14 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys.SMTP,
|
||||
keys.SMS,
|
||||
q,
|
||||
httpClient,
|
||||
)
|
||||
notification.Start(ctx)
|
||||
|
||||
execution.Register(
|
||||
ctx,
|
||||
config.Executions,
|
||||
httpClient,
|
||||
q,
|
||||
keys.Target,
|
||||
queries.GetActiveSigningWebKey,
|
||||
@@ -390,6 +394,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
keys,
|
||||
permissionCheck,
|
||||
cacheConnectors,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -435,6 +440,7 @@ func startAPIs(
|
||||
keys *encryption.EncryptionKeys,
|
||||
permissionCheck domain.PermissionCheck,
|
||||
cacheConnectors connector.Connectors,
|
||||
httpClient *http.Client,
|
||||
) (*api.API, error) {
|
||||
repo := struct {
|
||||
authz_repo.Repository
|
||||
@@ -492,7 +498,7 @@ func startAPIs(
|
||||
keys.Target,
|
||||
translator,
|
||||
config.Instrumentation.Trace.TrustRemoteSpans,
|
||||
config.Executions.DenyList,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating api %w", err)
|
||||
@@ -675,13 +681,29 @@ func startAPIs(
|
||||
config.Log.Slog(),
|
||||
config.SystemDefaults.SecretHasher,
|
||||
federatedLogoutsCache,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
|
||||
}
|
||||
apis.RegisterHandlerPrefixes(oidcServer, oidcPrefixes...)
|
||||
|
||||
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, keys.Target, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor)
|
||||
samlProvider, err := saml.NewProvider(
|
||||
config.SAML,
|
||||
config.ExternalSecure,
|
||||
commands,
|
||||
queries,
|
||||
authRepo,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
eventstore,
|
||||
dbClient,
|
||||
instanceInterceptor.Handler,
|
||||
userAgentInterceptor,
|
||||
limitingAccessInterceptor,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start saml provider: %w", err)
|
||||
}
|
||||
@@ -726,6 +748,7 @@ func startAPIs(
|
||||
keys.CSRFCookieKey,
|
||||
cacheConnectors,
|
||||
federatedLogoutsCache,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start login: %w", err)
|
||||
|
||||
@@ -5,22 +5,19 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func WithHTTP(ctx context.Context) Option {
|
||||
func WithHTTP(ctx context.Context, client *http.Client) Option {
|
||||
return func(c *runConfig) {
|
||||
c.modules["zitadel/http"] = func(runtime *goja.Runtime, module *goja.Object) {
|
||||
requireHTTP(ctx, &http.Client{Transport: &transport{lookup: net.LookupIP}}, runtime, module)
|
||||
requireHTTP(ctx, client, runtime, module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,18 +167,3 @@ func parseHeaders(headers *goja.Object) http.Header {
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
type transport struct {
|
||||
lookup func(string) ([]net.IP, error)
|
||||
}
|
||||
|
||||
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if httpConfig == nil || len(httpConfig.DenyList) == 0 {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
if err := denylist.IsHostBlocked(httpConfig.DenyList, req.URL, t.lookup); err != nil {
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "ACTIO-N72d0", "host is denied")
|
||||
}
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
)
|
||||
|
||||
func SetHTTPConfig(config *HTTPConfig) {
|
||||
httpConfig = config
|
||||
}
|
||||
|
||||
var httpConfig *HTTPConfig
|
||||
|
||||
type HTTPConfig struct {
|
||||
DenyList []denylist.AddressChecker
|
||||
}
|
||||
|
||||
+5
-6
@@ -26,7 +26,6 @@ import (
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -58,7 +57,7 @@ type API struct {
|
||||
targetEncryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
translator *i18n.Translator
|
||||
connectOTELInterceptor *otelconnect.Interceptor
|
||||
actionV2DenyList []denylist.AddressChecker
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func (a *API) ListGrpcServices() []string {
|
||||
@@ -113,7 +112,7 @@ func New(
|
||||
targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||
translator *i18n.Translator,
|
||||
trustRemoteSpans bool,
|
||||
deniedIPList []denylist.AddressChecker,
|
||||
httpClient *http.Client,
|
||||
) (_ *API, err error) {
|
||||
api := &API{
|
||||
port: port,
|
||||
@@ -129,10 +128,10 @@ func New(
|
||||
connectServices: make(map[string][]string),
|
||||
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
|
||||
translator: translator,
|
||||
actionV2DenyList: deniedIPList,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService(), targetEncryptionAlgorithm, api.translator, deniedIPList)
|
||||
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService(), targetEncryptionAlgorithm, api.translator, httpClient)
|
||||
api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -224,7 +223,7 @@ func (a *API) registerConnectServer(service server.ConnectServer) {
|
||||
connect_middleware.AuthorizationInterceptor(a.verifier, a.systemAuthZ, a.authConfig),
|
||||
connect_middleware.TranslationHandler(),
|
||||
connect_middleware.QuotaExhaustedInterceptor(a.accessInterceptor.AccessService(), system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
connect_middleware.ExecutionHandler(a.targetEncryptionAlgorithm, a.queries.GetActiveSigningWebKey, a.actionV2DenyList),
|
||||
connect_middleware.ExecutionHandler(a.targetEncryptionAlgorithm, a.queries.GetActiveSigningWebKey, a.httpClient),
|
||||
connect_middleware.ValidationHandler(),
|
||||
connect_middleware.ServiceHandler(),
|
||||
connect_middleware.ActivityInterceptor(),
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -27,12 +26,12 @@ var headersToForward = map[string]bool{
|
||||
strings.ToLower(http_utils.Origin): true,
|
||||
}
|
||||
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) connect.UnaryInterceptorFunc {
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) connect.UnaryInterceptorFunc {
|
||||
return func(handler connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) {
|
||||
|
||||
requestTargets := execution.QueryExecutionTargetsForRequest(ctx, req.Spec().Procedure)
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req, alg, activeSigningKey, deniedIPList)
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req, alg, activeSigningKey, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -43,12 +42,12 @@ func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution
|
||||
}
|
||||
|
||||
responseTargets := execution.QueryExecutionTargetsForResponse(ctx, req.Spec().Procedure)
|
||||
return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response, alg, activeSigningKey, deniedIPList)
|
||||
return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response, alg, activeSigningKey, httpClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ connect.AnyRequest, err error) {
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ connect.AnyRequest, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -68,14 +67,14 @@ func executeTargetsForRequest(ctx context.Context, targets []target_domain.Targe
|
||||
Headers: SetRequestHeaders(req.Header()),
|
||||
}
|
||||
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ connect.AnyResponse, err error) {
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ connect.AnyResponse, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -96,7 +95,7 @@ func executeTargetsForResponse(ctx context.Context, targets []target_domain.Targ
|
||||
Headers: SetRequestHeaders(req.Header()),
|
||||
}
|
||||
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
|
||||
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -75,9 +75,7 @@ func newMockContextInfoResponse(fullMethod, request, response string) *ContextIn
|
||||
}
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_request(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 target struct {
|
||||
reqBody execution.ContextInfo
|
||||
@@ -668,7 +666,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
tt.args.req,
|
||||
nil,
|
||||
tt.args.getActiveSigningWebKey,
|
||||
tt.args.deniedIPs,
|
||||
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
@@ -774,9 +772,7 @@ func validateJWEPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
}
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_response(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 target struct {
|
||||
reqBody execution.ContextInfo
|
||||
sleep time.Duration
|
||||
@@ -942,7 +938,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
tt.args.resp,
|
||||
nil,
|
||||
mockGetActiveSigningWebKey(),
|
||||
tt.args.deniedIPs,
|
||||
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
|
||||
@@ -13,17 +13,16 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
target_domain "github.com/zitadel/zitadel/internal/execution/target"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) grpc.UnaryServerInterceptor {
|
||||
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
requestTargets := execution.QueryExecutionTargetsForRequest(ctx, info.FullMethod)
|
||||
// call targets otherwise return req
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req, alg, activeSigningKey, deniedIPList)
|
||||
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req, alg, activeSigningKey, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,11 +33,11 @@ func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution
|
||||
}
|
||||
|
||||
responseTargets := execution.QueryExecutionTargetsForResponse(ctx, info.FullMethod)
|
||||
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response, alg, activeSigningKey, deniedIPList)
|
||||
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response, alg, activeSigningKey, httpClient)
|
||||
}
|
||||
}
|
||||
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req any, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ interface{}, err error) {
|
||||
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req any, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ interface{}, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -59,10 +58,10 @@ func executeTargetsForRequest(ctx context.Context, targets []target_domain.Targe
|
||||
Headers: connect_middleware.SetRequestHeaders(md),
|
||||
}
|
||||
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
|
||||
}
|
||||
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req, resp interface{}, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ interface{}, err error) {
|
||||
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req, resp interface{}, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ interface{}, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -84,7 +83,7 @@ func executeTargetsForResponse(ctx context.Context, targets []target_domain.Targ
|
||||
Headers: connect_middleware.SetRequestHeaders(md),
|
||||
}
|
||||
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
|
||||
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
|
||||
}
|
||||
|
||||
var _ execution.ContextInfo = &ContextInfoRequest{}
|
||||
|
||||
@@ -64,9 +64,7 @@ func newMockContextInfoResponse(fullMethod, request, response string) *ContextIn
|
||||
}
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_request(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 target struct {
|
||||
reqBody execution.ContextInfo
|
||||
@@ -656,7 +654,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
|
||||
tt.args.req,
|
||||
nil,
|
||||
tt.args.getActiveSigningWebKey,
|
||||
tt.args.deniedIPs,
|
||||
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
@@ -762,9 +760,7 @@ func validateJWEPayload(t *testing.T) func(expected, sent []byte) bool {
|
||||
}
|
||||
|
||||
func Test_executeTargetsForGRPCFullMethod_response(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 target struct {
|
||||
reqBody execution.ContextInfo
|
||||
@@ -930,7 +926,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
|
||||
tt.args.resp,
|
||||
nil,
|
||||
mockGetActiveSigningWebKey(),
|
||||
tt.args.deniedIPs,
|
||||
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
|
||||
)
|
||||
|
||||
if tt.res.wantErr {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
grpc_api "github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/logstore/record"
|
||||
@@ -69,7 +68,7 @@ func CreateServer(
|
||||
accessSvc *logstore.Service[*record.AccessLog],
|
||||
targetEncAlg crypto.EncryptionAlgorithm,
|
||||
translator *i18n.Translator,
|
||||
deniedIPList []denylist.AddressChecker,
|
||||
httpClient *http.Client,
|
||||
) *grpc.Server {
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode}
|
||||
serverOptions := []grpc.ServerOption{
|
||||
@@ -87,7 +86,7 @@ func CreateServer(
|
||||
middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig),
|
||||
middleware.TranslationHandler(),
|
||||
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
middleware.ExecutionHandler(targetEncAlg, queries.GetActiveSigningWebKey, deniedIPList),
|
||||
middleware.ExecutionHandler(targetEncAlg, queries.GetActiveSigningWebKey, httpClient),
|
||||
middleware.ValidationHandler(),
|
||||
middleware.ServiceHandler(),
|
||||
middleware.ActivityInterceptor(),
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
MaxBodySize int64
|
||||
Timeout time.Duration
|
||||
MaxRedirects int
|
||||
AllowHTTPSDowngrade bool
|
||||
DenyList []denylist.AddressChecker
|
||||
}
|
||||
|
||||
// NewClient returns a new http.Client with the configured settings.
|
||||
// The client is protected against DNS rebinding attacks, redirects, HTTPs downgrades, and response body size limits.
|
||||
func (c *ClientConfig) NewClient() *http.Client {
|
||||
baseTransport := denylist.NewHTTPTransport(c.DenyList)
|
||||
|
||||
protectedTransport := &MaxBytesRoundTripper{
|
||||
Underlying: baseTransport,
|
||||
MaxBytes: c.MaxBodySize,
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: protectedTransport,
|
||||
Timeout: c.Timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= c.MaxRedirects {
|
||||
return ErrTooManyRedirects
|
||||
}
|
||||
if !c.AllowHTTPSDowngrade && len(via) > 0 {
|
||||
prev := via[len(via)-1]
|
||||
if prev != nil && prev.URL != nil && req.URL != nil &&
|
||||
strings.EqualFold(prev.URL.Scheme, "https") &&
|
||||
!strings.EqualFold(req.URL.Scheme, "https") {
|
||||
return ErrHTTPsDowngrade
|
||||
}
|
||||
}
|
||||
return denylist.IsURLBlocked(c.DenyList, req.URL, nil)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MergeDeprecatedDenylists merges the two deprecated (actions) denylists into the main denylist.
|
||||
func (c *ClientConfig) MergeDeprecatedDenylists(actionsV1, actionsV2 []denylist.AddressChecker) {
|
||||
c.DenyList = append(c.DenyList, actionsV1...)
|
||||
c.DenyList = append(c.DenyList, actionsV2...)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrResponseTooLarge is returned when the response body exceeds the configured limit.
|
||||
ErrResponseTooLarge = errors.New("response body exceeded maximum allowed size")
|
||||
//ErrTooManyRedirects is returned when the number of redirects exceeds the configured limit.
|
||||
ErrTooManyRedirects = errors.New("stopped after too many redirects")
|
||||
// ErrHTTPsDowngrade is returned when the client attempts to downgrade to HTTP.
|
||||
ErrHTTPsDowngrade = errors.New("redirect downgrade from https to http is not allowed")
|
||||
)
|
||||
|
||||
// MaxBytesRoundTripper wraps an existing RoundTripper to protect against OOM.
|
||||
type MaxBytesRoundTripper struct {
|
||||
Underlying http.RoundTripper
|
||||
MaxBytes int64
|
||||
}
|
||||
|
||||
func (m *MaxBytesRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
transport := m.Underlying
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
|
||||
resp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.ContentLength > m.MaxBytes {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("%w: Content-Length is %d (limit %d)", ErrResponseTooLarge, resp.ContentLength, m.MaxBytes)
|
||||
}
|
||||
|
||||
resp.Body = &strictMaxBytesReader{
|
||||
limitReader: io.LimitReader(resp.Body, m.MaxBytes+1), // We initialize with +1 to detect overflows during Read
|
||||
closer: resp.Body,
|
||||
limit: m.MaxBytes,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = (*MaxBytesRoundTripper)(nil)
|
||||
|
||||
// strictMaxBytesReader enforces a hard limit and returns an explicit error if exceeded.
|
||||
type strictMaxBytesReader struct {
|
||||
limitReader io.Reader
|
||||
closer io.Closer
|
||||
limit int64
|
||||
bytesRead int64
|
||||
}
|
||||
|
||||
func (s *strictMaxBytesReader) Read(p []byte) (int, error) {
|
||||
n, err := s.limitReader.Read(p)
|
||||
s.bytesRead += int64(n)
|
||||
|
||||
if s.bytesRead > s.limit {
|
||||
// Because LimitReader stops at limit+1, excess is mathematically guaranteed to be 1
|
||||
safeN := n - 1
|
||||
if safeN < 0 {
|
||||
safeN = 0
|
||||
}
|
||||
|
||||
// This instantly slices off that single offending byte from the view of the caller
|
||||
p = p[:safeN]
|
||||
return safeN, ErrResponseTooLarge
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *strictMaxBytesReader) Close() error {
|
||||
return s.closer.Close()
|
||||
}
|
||||
|
||||
var _ io.ReadCloser = (*strictMaxBytesReader)(nil)
|
||||
@@ -0,0 +1,144 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/denylist"
|
||||
)
|
||||
|
||||
func TestClientConfig_NewClient_Allowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Spin up a simple local test server to serve a dummy response
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hello world"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// An empty denylist permits all safe destinations
|
||||
cfg := &ClientConfig{
|
||||
MaxBodySize: 1024,
|
||||
Timeout: 2 * time.Second,
|
||||
MaxRedirects: 3,
|
||||
DenyList: []denylist.AddressChecker{},
|
||||
}
|
||||
|
||||
client := cfg.NewClient()
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConfig_Redirect_HTTPSDowngradeBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &ClientConfig{
|
||||
MaxBodySize: 1024,
|
||||
Timeout: 2 * time.Second,
|
||||
MaxRedirects: 3,
|
||||
AllowHTTPSDowngrade: false, // Explicitly disallow downgrades
|
||||
DenyList: []denylist.AddressChecker{},
|
||||
}
|
||||
|
||||
client := cfg.NewClient()
|
||||
|
||||
// Mock a historical request chain moving from a secure schema to an unencrypted target
|
||||
viaReq, _ := http.NewRequest(http.MethodGet, "https://secure-identity.com/oauth", nil)
|
||||
currentReq, _ := http.NewRequest(http.MethodGet, "http://insecure-identity.com/callback", nil)
|
||||
|
||||
err := client.CheckRedirect(currentReq, []*http.Request{viaReq})
|
||||
assert.ErrorIs(t, err, ErrHTTPsDowngrade)
|
||||
}
|
||||
|
||||
func TestMaxBytesRoundTripper_EnforcesCap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Spin up a local server delivering an exact 10-byte payload response string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("0123456789"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
maxBytes int64
|
||||
expectError error
|
||||
}{
|
||||
{
|
||||
name: "body cleanly within threshold limit",
|
||||
maxBytes: 15,
|
||||
expectError: nil,
|
||||
},
|
||||
{
|
||||
name: "body matches boundary size exactly",
|
||||
maxBytes: 10,
|
||||
expectError: nil,
|
||||
},
|
||||
{
|
||||
name: "body exceeds allowable limit",
|
||||
maxBytes: 5,
|
||||
expectError: ErrResponseTooLarge,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &ClientConfig{
|
||||
MaxBodySize: tt.maxBytes,
|
||||
Timeout: 2 * time.Second,
|
||||
MaxRedirects: 3,
|
||||
DenyList: []denylist.AddressChecker{},
|
||||
}
|
||||
|
||||
client := cfg.NewClient()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if tt.expectError != nil {
|
||||
// Depending on optimization, errors can trip directly on call or during chunk reading
|
||||
if err != nil {
|
||||
assert.ErrorIs(t, err, tt.expectError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, readErr := io.ReadAll(resp.Body)
|
||||
assert.ErrorIs(t, readErr, tt.expectError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, readErr)
|
||||
assert.Equal(t, 10, len(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConfig_MergeDeprecatedDenylists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &ClientConfig{DenyList: []denylist.AddressChecker{denylist.NewHostChecker("1.1.1.1")}}
|
||||
|
||||
v1 := []denylist.AddressChecker{denylist.NewHostChecker("2.2.2.2")}
|
||||
v2 := []denylist.AddressChecker{denylist.NewHostChecker("3.3.3.3")}
|
||||
|
||||
cfg.MergeDeprecatedDenylists(v1, v2)
|
||||
|
||||
assert.Len(t, cfg.DenyList, 3)
|
||||
}
|
||||
@@ -127,6 +127,7 @@ func NewServer(
|
||||
fallbackLogger *slog.Logger,
|
||||
hashConfig crypto.HashConfig,
|
||||
federatedLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
|
||||
httpClient *http.Client,
|
||||
) (*Server, error) {
|
||||
opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey)
|
||||
if err != nil {
|
||||
@@ -189,6 +190,7 @@ func NewServer(
|
||||
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
|
||||
opCrypto: alg,
|
||||
assetAPIPrefix: assets.AssetAPI(),
|
||||
httpClient: httpClient,
|
||||
}
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||
server.Handler = op.RegisterLegacyServer(server,
|
||||
|
||||
@@ -44,6 +44,7 @@ type Server struct {
|
||||
opCrypto op.Crypto
|
||||
|
||||
assetAPIPrefix func(ctx context.Context) string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func endpoints(endpointConfig *EndpointConfig) op.Endpoints {
|
||||
|
||||
@@ -424,7 +424,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
|
||||
apiFields,
|
||||
action.Script,
|
||||
action.Name,
|
||||
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
||||
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx, s.httpClient), actions.WithUUID(actionCtx))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -461,7 +461,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
|
||||
UserGrants: qu.UserGrants,
|
||||
}
|
||||
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, s.targetEncryptionAlgorithm, s.query.GetActiveSigningWebKey, s.command.ActionsV2DenyList)
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, s.targetEncryptionAlgorithm, s.query.GetActiveSigningWebKey, s.httpClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ func NewProvider(
|
||||
instanceHandler,
|
||||
userAgentCookie func(http.Handler) http.Handler,
|
||||
accessHandler *middleware.AccessInterceptor,
|
||||
httpClient *http.Client,
|
||||
) (*Provider, error) {
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||
|
||||
@@ -63,6 +64,7 @@ func NewProvider(
|
||||
fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
||||
conf.DefaultLoginURLV2,
|
||||
ContextToIssuer,
|
||||
httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -125,6 +127,7 @@ func newStorage(
|
||||
defaultLoginURL string,
|
||||
defaultLoginURLV2 string,
|
||||
contextToIssuer func(context.Context) string,
|
||||
actionHttpClient *http.Client,
|
||||
) (*Storage, error) {
|
||||
return &Storage{
|
||||
encAlg: encAlg,
|
||||
@@ -138,6 +141,7 @@ func newStorage(
|
||||
defaultLoginURL: defaultLoginURL,
|
||||
defaultLoginURLv2: defaultLoginURLV2,
|
||||
contextToIssuer: contextToIssuer,
|
||||
httpClient: actionHttpClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -66,6 +67,7 @@ type Storage struct {
|
||||
defaultLoginURL string
|
||||
defaultLoginURLv2 string
|
||||
contextToIssuer func(context.Context) string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
|
||||
@@ -380,7 +382,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
|
||||
apiFields,
|
||||
action.Script,
|
||||
action.Name,
|
||||
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx))...,
|
||||
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx, p.httpClient))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -401,7 +403,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
|
||||
UserGrants: userGrants.UserGrants,
|
||||
}
|
||||
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, p.targetEncAlg, p.query.GetActiveSigningWebKey, p.command.ActionsV2DenyList)
|
||||
resp, err := execution.CallTargets(ctx, executionTargets, info, p.targetEncAlg, p.query.GetActiveSigningWebKey, p.httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (l *Login) runPostExternalAuthenticationActions(
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -226,7 +226,7 @@ func (l *Login) runPostInternalAuthenticationActions(
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -346,7 +346,7 @@ func (l *Login) runPreCreationActions(
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -423,7 +423,7 @@ func (l *Login) runPostCreationActions(
|
||||
apiFields,
|
||||
a.Script,
|
||||
a.Name,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
||||
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
|
||||
)
|
||||
cancel()
|
||||
if err != nil {
|
||||
|
||||
@@ -1053,6 +1053,7 @@ func (l *Login) googleProvider(ctx context.Context, identityProvider *query.IDPT
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GoogleIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1079,6 +1080,7 @@ func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.OIDCIDPTemplate.Scopes,
|
||||
openid.DefaultMapper,
|
||||
l.httpClient,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
@@ -1092,6 +1094,7 @@ func (l *Login) jwtProvider(identityProvider *query.IDPTemplate) (*jwt.Provider,
|
||||
identityProvider.JWTIDPTemplate.HeaderName,
|
||||
identityProvider.JWTIDPTemplate.Audience,
|
||||
l.idpConfigAlg,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1123,6 +1126,7 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
func() idp.User {
|
||||
return oauth.NewUserMapper(identityProvider.OAuthIDPTemplate.IDAttribute)
|
||||
},
|
||||
l.httpClient,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
@@ -1176,6 +1180,7 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem
|
||||
identityProvider.Metadata,
|
||||
identityProvider.Certificate,
|
||||
key,
|
||||
l.httpClient,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
@@ -1198,6 +1203,7 @@ func (l *Login) azureProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.AzureADIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
@@ -1212,6 +1218,7 @@ func (l *Login) githubProvider(ctx context.Context, identityProvider *query.IDPT
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitHubIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1229,6 +1236,7 @@ func (l *Login) githubEnterpriseProvider(ctx context.Context, identityProvider *
|
||||
identityProvider.GitHubEnterpriseIDPTemplate.TokenEndpoint,
|
||||
identityProvider.GitHubEnterpriseIDPTemplate.UserEndpoint,
|
||||
identityProvider.GitHubIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1242,6 +1250,7 @@ func (l *Login) gitlabProvider(ctx context.Context, identityProvider *query.IDPT
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitLabIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1257,6 +1266,7 @@ func (l *Login) gitlabSelfHostedProvider(ctx context.Context, identityProvider *
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GitLabSelfHostedIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1272,6 +1282,7 @@ func (l *Login) appleProvider(ctx context.Context, identityProvider *query.IDPTe
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallbackFormPost,
|
||||
privateKey,
|
||||
identityProvider.AppleIDPTemplate.Scopes,
|
||||
l.httpClient,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ type Login struct {
|
||||
idpConfigAlg crypto.EncryptionAlgorithm
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
caches *Caches
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -198,6 +199,7 @@ func CreateLogin(
|
||||
csrfCookieKey []byte,
|
||||
cacheConnectors connector.Connectors,
|
||||
federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
|
||||
httpClient *http.Client,
|
||||
) (*Login, error) {
|
||||
login := &Login{
|
||||
oidcAuthCallbackURL: oidcAuthCallbackURL,
|
||||
@@ -210,6 +212,7 @@ func CreateLogin(
|
||||
authRepo: authRepo,
|
||||
idpConfigAlg: idpConfigAlg,
|
||||
userCodeAlg: userCodeAlg,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
|
||||
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,10 +14,7 @@ type AddressChecker interface {
|
||||
func ParseDenyList(inputList []string) ([]AddressChecker, error) {
|
||||
var toReturn []AddressChecker
|
||||
for _, input := range inputList {
|
||||
parsed, parseErr := NewHostChecker(input)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
parsed := NewHostChecker(input)
|
||||
if parsed != nil {
|
||||
toReturn = append(toReturn, parsed)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package denylist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseDenyList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tt := []struct {
|
||||
testName string
|
||||
inputIPList []string
|
||||
expectedAddressCheckers int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "when empty list should return no error and no checker list",
|
||||
},
|
||||
{
|
||||
testName: "list with a non-ip should return no error and no checker list",
|
||||
inputIPList: []string{"127.0.0.1", "localhost", "not an ip"},
|
||||
expectedAddressCheckers: 3,
|
||||
},
|
||||
{
|
||||
testName: "parseable IP list should return no error, non-zero hash and non-empty checker list",
|
||||
inputIPList: []string{"127.0.0.1", "localhost", "1.1.1.1"},
|
||||
expectedAddressCheckers: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test
|
||||
checkers, err := ParseDenyList(tc.inputIPList)
|
||||
|
||||
// Verify
|
||||
assert.Len(t, checkers, tc.expectedAddressCheckers)
|
||||
assert.ErrorIs(t, err, tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package denylist
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
internal_net "github.com/zitadel/zitadel/internal/net"
|
||||
)
|
||||
@@ -15,23 +16,30 @@ type HostChecker struct {
|
||||
Domain string
|
||||
}
|
||||
|
||||
func NewHostChecker(entry string) (AddressChecker, error) {
|
||||
// NewHostChecker returns an [AddressChecker].
|
||||
//
|
||||
// If the input string is neither a network, nor an IP address, it is considered a domain and saved as such
|
||||
func NewHostChecker(entry string) AddressChecker {
|
||||
if entry == "" {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
_, network, err := net.ParseCIDR(entry)
|
||||
if err == nil {
|
||||
return &HostChecker{Net: network}, nil
|
||||
return &HostChecker{Net: network}
|
||||
}
|
||||
if ip := net.ParseIP(entry); ip != nil {
|
||||
return &HostChecker{IP: ip}, nil
|
||||
return &HostChecker{IP: ip}
|
||||
}
|
||||
return &HostChecker{Domain: entry}, nil
|
||||
return &HostChecker{Domain: entry}
|
||||
}
|
||||
|
||||
// IsDenied checks if the address or one of the IPs is blocked by the [HostChecker].
|
||||
// The address is checked using string comparison.
|
||||
// If either is blocked, an [AddressDeniedError] will be returned.
|
||||
// If either is not blocked, it will return nil.
|
||||
func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
|
||||
// if the address matches the domain, no additional checks as needed
|
||||
if c.Domain == address {
|
||||
if c.Domain != "" && strings.EqualFold(c.Domain, address) {
|
||||
return NewAddressDeniedError(c.Domain)
|
||||
}
|
||||
// otherwise we need to check on ips (incl. the resolved ips of the host)
|
||||
@@ -46,20 +54,9 @@ func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsHostBlocked checks address against denyList. If a match is found, an [AddressDeniedError] will be returned
|
||||
// Takes an input lookupFunc to convert address to a list of IP addresses.
|
||||
// If lookupFunc is nil, it defaults to [net.LookupIP]
|
||||
func IsHostBlocked(denyList []AddressChecker, address *url.URL, lookupFunc internal_net.IPLookupFunc) error {
|
||||
if lookupFunc == nil {
|
||||
lookupFunc = net.LookupIP
|
||||
}
|
||||
|
||||
host := address.Hostname()
|
||||
ips, err := internal_net.HostnameToIPList(host, lookupFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// IsHostBlocked checks address against denyList. If a match is found, an [AddressDeniedError] will be returned.
|
||||
// If no match is found, it will return nil.
|
||||
func IsHostBlocked(denyList []AddressChecker, host string, ips ...net.IP) error {
|
||||
for _, denied := range denyList {
|
||||
if err := denied.IsDenied(ips, host); err != nil {
|
||||
return err
|
||||
@@ -67,3 +64,26 @@ func IsHostBlocked(denyList []AddressChecker, address *url.URL, lookupFunc inter
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsHostnameBlocked checks a hostname against the denyList. If a match is found, an [AddressDeniedError] will be returned
|
||||
// It takes an input lookupFunc to convert the hostname to a list of IP addresses.
|
||||
// If lookupFunc is nil, it defaults to [net.LookupIP]
|
||||
func IsHostnameBlocked(denyList []AddressChecker, hostname string, lookupFunc internal_net.IPLookupFunc) error {
|
||||
if lookupFunc == nil {
|
||||
lookupFunc = net.LookupIP
|
||||
}
|
||||
|
||||
ips, err := internal_net.HostnameToIPList(hostname, lookupFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return IsHostBlocked(denyList, hostname, ips...)
|
||||
}
|
||||
|
||||
// IsURLBlocked checks a URL against denyList. If a match is found, an [AddressDeniedError] will be returned
|
||||
// It takes an input lookupFunc to convert the URL's hostname to a list of IP addresses.
|
||||
// If lookupFunc is nil, it defaults to [net.LookupIP]
|
||||
func IsURLBlocked(denyList []AddressChecker, address *url.URL, lookupFunc internal_net.IPLookupFunc) error {
|
||||
return IsHostnameBlocked(denyList, address.Hostname(), lookupFunc)
|
||||
}
|
||||
|
||||
@@ -7,23 +7,25 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestIsHostBlocked(t *testing.T) {
|
||||
func TestIsURLBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
var denyList = []AddressChecker{
|
||||
mustNewHostChecker(t, "192.168.5.0/24"),
|
||||
mustNewHostChecker(t, "127.0.0.1"),
|
||||
mustNewHostChecker(t, "test.com"),
|
||||
|
||||
denyList := []AddressChecker{
|
||||
NewHostChecker("192.168.5.0/24"),
|
||||
NewHostChecker("127.0.0.1"),
|
||||
NewHostChecker("test.com"),
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
lookup func(host string) ([]net.IP, error)
|
||||
}
|
||||
type args struct {
|
||||
address *url.URL
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
@@ -31,21 +33,31 @@ func TestIsHostBlocked(t *testing.T) {
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "in range",
|
||||
name: "in range (CIDR match)",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("192.168.5.4")}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
address: mustNewURL(t, "https://192.168.5.4/hodor"),
|
||||
},
|
||||
want: NewAddressDeniedError("192.168.5.0/24"),
|
||||
},
|
||||
{
|
||||
name: "exact ip",
|
||||
name: "exact ip literal match",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
|
||||
},
|
||||
want: NewAddressDeniedError("127.0.0.1"),
|
||||
},
|
||||
{
|
||||
name: "address match",
|
||||
name: "domain string match",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
||||
@@ -57,7 +69,7 @@ func TestIsHostBlocked(t *testing.T) {
|
||||
want: NewAddressDeniedError("test.com"),
|
||||
},
|
||||
{
|
||||
name: "address not match",
|
||||
name: "allowed domain string pass",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
||||
@@ -69,7 +81,7 @@ func TestIsHostBlocked(t *testing.T) {
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "looked up ip matches",
|
||||
name: "unmatched domain resolves to blocked IP (SSRF pivot attempt)",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
||||
@@ -81,7 +93,7 @@ func TestIsHostBlocked(t *testing.T) {
|
||||
want: NewAddressDeniedError("127.0.0.1"),
|
||||
},
|
||||
{
|
||||
name: "lookup failure",
|
||||
name: "dns resolver failures are isolated",
|
||||
fields: fields{
|
||||
lookup: func(host string) ([]net.IP, error) {
|
||||
return nil, io.EOF
|
||||
@@ -93,23 +105,48 @@ func TestIsHostBlocked(t *testing.T) {
|
||||
want: zerrors.ThrowInternal(io.EOF, "NET-4m9s2", "lookup failed"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := IsHostBlocked(denyList, tt.args.address, tt.fields.lookup)
|
||||
got := IsURLBlocked(denyList, tt.args.address, tt.fields.lookup)
|
||||
assert.ErrorIs(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewHostChecker(t *testing.T, ip string) AddressChecker {
|
||||
t.Helper()
|
||||
checker, err := NewHostChecker(ip)
|
||||
if err != nil {
|
||||
t.Errorf("unable to parse cidr of %q because: %v", ip, err)
|
||||
t.FailNow()
|
||||
func TestIsHostnameBlocked_EdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
denyList := []AddressChecker{
|
||||
NewHostChecker("127.0.0.1"),
|
||||
NewHostChecker("blocked.example.com"),
|
||||
}
|
||||
return checker
|
||||
|
||||
t.Run("blocked by host lookup via hostname method", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := IsHostnameBlocked(denyList, "safe.example.com", func(_ string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
||||
})
|
||||
assert.ErrorIs(t, err, NewAddressDeniedError("127.0.0.1"))
|
||||
})
|
||||
|
||||
t.Run("blocked by exact domain string mapping", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := IsHostnameBlocked(denyList, "blocked.example.com", func(_ string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("192.0.2.1")}, nil
|
||||
})
|
||||
assert.ErrorIs(t, err, NewAddressDeniedError("blocked.example.com"))
|
||||
})
|
||||
|
||||
t.Run("ipv6 target check compatibility", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// URL parsing or raw string handling yields plain IPv6 targets, make sure checker behaves cleanly
|
||||
err := IsHostnameBlocked(denyList, "::1", func(_ string) ([]net.IP, error) {
|
||||
return []net.IP{net.ParseIP("::1")}, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func mustNewURL(t *testing.T, raw string) *url.URL {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package denylist
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewHTTPTransport returns a cloned default transport that enforces denylist checks
|
||||
// right before each TCP dial to avoid DNS rebinding TOCTOU gaps.
|
||||
func NewHTTPTransport(denyList []AddressChecker) http.RoundTripper {
|
||||
base := http.DefaultTransport.(*http.Transport).Clone()
|
||||
if len(denyList) == 0 {
|
||||
return base
|
||||
}
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedIP := net.ParseIP(host)
|
||||
if parsedIP == nil { // at this point, it must be an IP so it should never happen
|
||||
return &net.DNSError{Err: "invalid IP address", Name: host}
|
||||
}
|
||||
|
||||
return IsHostBlocked(denyList, host, parsedIP)
|
||||
},
|
||||
}
|
||||
base.DialContext = dialer.DialContext
|
||||
|
||||
return base
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package denylist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewHTTPTransport_Allowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a denylist that does NOT block our local test server
|
||||
denyList := []AddressChecker{
|
||||
NewHostChecker("192.168.1.0/24"),
|
||||
NewHostChecker("10.0.0.1"),
|
||||
}
|
||||
|
||||
// Spin up a dummy local test server. It will listen on a loopback address like 127.0.0.1 or ::1
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("success"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Instantiate the transport with the denylist rules
|
||||
transport := NewHTTPTransport(denyList)
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// Execute the request to the allowed local server target
|
||||
resp, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPTransport_BlockedByControl(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Spin up a test server so we can dynamically extract its specific local listening loopback IP
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Parse out the target host/ip text from the server URL
|
||||
u, err := net.Dial("tcp", server.Listener.Addr().String())
|
||||
assert.NoError(t, err)
|
||||
localIP, _, err := net.SplitHostPort(u.RemoteAddr().String())
|
||||
assert.NoError(t, err)
|
||||
_ = u.Close()
|
||||
|
||||
// Add the explicitly discovered test server IP to the denylist
|
||||
denyList := []AddressChecker{
|
||||
NewHostChecker(localIP),
|
||||
}
|
||||
|
||||
transport := NewHTTPTransport(denyList)
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// This must fail because the Control hook interceptor throws an address denied error
|
||||
// right before the low-level TCP handshake begins.
|
||||
_, err = client.Get(server.URL) //nolint:bodyclose
|
||||
assert.Error(t, err)
|
||||
|
||||
// Because Go wraps dialer context execution errors inside a generic url.Error,
|
||||
// we verify that the true root cause matches our address denial definition.
|
||||
var addressDeniedErr *AddressDeniedError
|
||||
assert.ErrorAs(t, err, &addressDeniedErr)
|
||||
}
|
||||
|
||||
func TestNewHTTPTransport_EmptyDenylistReturnsDefaultClone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// An empty list should bypass adding a dialer wrapper entirely for performance
|
||||
transport := NewHTTPTransport([]AddressChecker{})
|
||||
|
||||
assert.NotNil(t, transport)
|
||||
httpTransport, ok := transport.(*http.Transport)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Ensure that DialContext is still populated (inherited cleanly from http.DefaultTransport Clone)
|
||||
assert.NotNil(t, httpTransport.DialContext)
|
||||
}
|
||||
|
||||
func TestNewHTTPTransport_MalformedAddressHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
denyList := []AddressChecker{
|
||||
NewHostChecker("127.0.0.1"),
|
||||
}
|
||||
|
||||
transport := NewHTTPTransport(denyList)
|
||||
httpTransport, ok := transport.(*http.Transport)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Explicitly invoke DialContext with an unparseable address format (missing a valid port boundary)
|
||||
// to test how the underlying Control code deals with structural string parsing failure exceptions.
|
||||
_, err := httpTransport.DialContext(context.Background(), "tcp", "malformed-address-without-port")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Ensure it didn't crash and returns a clean, standard connection framework error context
|
||||
var netErr net.Error
|
||||
assert.True(t, errors.As(err, &netErr))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -61,6 +61,7 @@ func newExecutionWorker(f fieldsWorker) *execution.Worker {
|
||||
nil,
|
||||
mockGetActiveSigningWebKey,
|
||||
f.now,
|
||||
http.DefaultClient,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package apple
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
@@ -25,13 +26,13 @@ type Provider struct {
|
||||
*oidc.Provider
|
||||
}
|
||||
|
||||
func New(clientID, teamID, keyID, callbackURL string, key []byte, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
func New(clientID, teamID, keyID, callbackURL string, key []byte, scopes []string, httpClient *http.Client, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
secret, err := clientSecretFromPrivateKey(key, teamID, clientID, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options = append(options, oidc.WithResponseMode("form_post"))
|
||||
rp, err := oidc.New(name, issuer, clientID, secret, callbackURL, scopes, oidc.DefaultMapper, options...)
|
||||
rp, err := oidc.New(name, issuer, clientID, secret, callbackURL, scopes, oidc.DefaultMapper, httpClient, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package apple
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -57,7 +58,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes, http.DefaultClient)
|
||||
r.NoError(err)
|
||||
ctx := context.Background()
|
||||
session, err := provider.BeginAuth(ctx, "testState")
|
||||
|
||||
@@ -3,6 +3,7 @@ package apple
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -156,7 +157,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
|
||||
// call the real discovery endpoint
|
||||
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
|
||||
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes, http.DefaultClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
|
||||
@@ -2,6 +2,7 @@ package azuread
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -88,7 +89,7 @@ func WithOAuthOptions(opts ...oauth.ProviderOpts) ProviderOptions {
|
||||
|
||||
// New creates an AzureAD provider using the [oauth.Provider] (OAuth 2.0 generic provider).
|
||||
// By default, it uses the [CommonTenant] and unverified emails.
|
||||
func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts ...ProviderOptions) (*Provider, error) {
|
||||
func New(name, clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, opts ...ProviderOptions) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
tenant: CommonTenant,
|
||||
options: make([]oauth.ProviderOpts, 0),
|
||||
@@ -104,6 +105,7 @@ func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts
|
||||
func() idp.User {
|
||||
return &User{isEmailVerified: provider.emailVerified}
|
||||
},
|
||||
httpClient,
|
||||
provider.options...,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package azuread
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -74,7 +75,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
|
||||
r.NoError(err)
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -169,7 +170,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
a.Equal(tt.want.name, provider.Name())
|
||||
|
||||
@@ -244,7 +244,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
tt.fields.httpMock()
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
@@ -395,7 +395,7 @@ func TestSession_RetrievePreviousID(t *testing.T) {
|
||||
tt.fields.httpMock()
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient)
|
||||
require.NoError(t, err)
|
||||
session := &Session{
|
||||
Provider: provider,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -23,13 +24,13 @@ const (
|
||||
var _ idp.Provider = (*Provider)(nil)
|
||||
|
||||
// New creates a GitHub.com provider using the [oauth.Provider] (OAuth 2.0 generic provider)
|
||||
func New(clientID, secret, callbackURL string, scopes []string, options ...oauth.ProviderOpts) (*Provider, error) {
|
||||
return NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL, scopes, options...)
|
||||
func New(clientID, secret, callbackURL string, scopes []string, httpClient *http.Client, options ...oauth.ProviderOpts) (*Provider, error) {
|
||||
return NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL, scopes, httpClient, options...)
|
||||
}
|
||||
|
||||
// NewCustomURL creates a GitHub provider using the [oauth.Provider] (OAuth 2.0 generic provider)
|
||||
// with custom endpoints, e.g. GitHub Enterprise server
|
||||
func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL string, scopes []string, options ...oauth.ProviderOpts) (*Provider, error) {
|
||||
func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL string, scopes []string, httpClient *http.Client, options ...oauth.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oauth.New(
|
||||
newConfig(clientID, secret, callbackURL, authURL, tokenURL, scopes),
|
||||
name,
|
||||
@@ -37,6 +38,7 @@ func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profil
|
||||
func() idp.User {
|
||||
return new(User)
|
||||
},
|
||||
httpClient,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -41,7 +42,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
|
||||
r.NoError(err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -395,7 +395,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
}
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
openid "github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
@@ -20,13 +22,13 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// New creates a GitLab.com provider using the [oidc.Provider] (OIDC generic provider)
|
||||
func New(clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
||||
func New(clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, scopes, httpClient, options...)
|
||||
}
|
||||
|
||||
// NewCustomIssuer creates a GitLab provider using the [oidc.Provider] (OIDC generic provider)
|
||||
// with a custom issuer for self-managed instances
|
||||
func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
if len(scopes) == 0 {
|
||||
// the OIDC provider would set `openid profile email phone` as default scope,
|
||||
// but since gitlab does not handle unknown scopes correctly (phone) and returns an error,
|
||||
@@ -35,7 +37,7 @@ func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, s
|
||||
}
|
||||
// gitlab is currently not able to handle the prompt `select_account`:
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/377368
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, oidc.DefaultMapper, options...)
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, oidc.DefaultMapper, httpClient, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package gitlab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -53,7 +54,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.opts...)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.opts...)
|
||||
r.NoError(err)
|
||||
ctx := context.Background()
|
||||
session, err := provider.BeginAuth(ctx, "testState")
|
||||
|
||||
@@ -3,6 +3,7 @@ package gitlab
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -166,7 +167,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
|
||||
// call the real discovery endpoint
|
||||
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &oidc.Session{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
openid "github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
@@ -20,8 +22,8 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// New creates a Google provider using the [oidc.Provider] (OIDC generic provider)
|
||||
func New(clientID, clientSecret, redirectURI string, scopes []string, opts ...oidc.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, userMapper, append(opts, oidc.WithSelectAccount())...)
|
||||
func New(clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, opts ...oidc.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, userMapper, httpClient, append(opts, oidc.WithSelectAccount())...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -41,7 +42,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient)
|
||||
r.NoError(err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -3,6 +3,7 @@ package google
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -167,7 +168,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
|
||||
// call the real discovery endpoint
|
||||
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &oidc.Session{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@@ -34,6 +35,7 @@ type Provider struct {
|
||||
isAutoCreation bool
|
||||
isAutoUpdate bool
|
||||
encryptionAlg crypto.EncryptionAlgorithm
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type ProviderOpts func(provider *Provider)
|
||||
@@ -68,7 +70,7 @@ func WithAutoUpdate() ProviderOpts {
|
||||
}
|
||||
|
||||
// New creates a JWT provider
|
||||
func New(name, issuer, jwtEndpoint, keysEndpoint, headerName, audience string, encryptionAlg crypto.EncryptionAlgorithm, options ...ProviderOpts) (*Provider, error) {
|
||||
func New(name, issuer, jwtEndpoint, keysEndpoint, headerName, audience string, encryptionAlg crypto.EncryptionAlgorithm, httpClient *http.Client, options ...ProviderOpts) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
name: name,
|
||||
issuer: issuer,
|
||||
@@ -77,6 +79,7 @@ func New(name, issuer, jwtEndpoint, keysEndpoint, headerName, audience string, e
|
||||
headerName: headerName,
|
||||
audience: audience,
|
||||
encryptionAlg: encryptionAlg,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(provider)
|
||||
|
||||
@@ -3,6 +3,7 @@ package jwt
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -114,6 +115,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
tt.fields.headerName,
|
||||
tt.fields.audience,
|
||||
tt.fields.encryptionAlg(t),
|
||||
http.DefaultClient,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -221,6 +223,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
tt.fields.headerName,
|
||||
tt.fields.audience,
|
||||
tt.fields.encryptionAlg(t),
|
||||
http.DefaultClient,
|
||||
tt.fields.opts...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDToke
|
||||
}
|
||||
|
||||
logging.Debug("begin signature validation")
|
||||
keySet := rp.NewRemoteKeySet(http.DefaultClient, s.Provider.keysEndpoint)
|
||||
keySet := rp.NewRemoteKeySet(s.httpClient, s.keysEndpoint)
|
||||
if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid signature: %v", ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -378,6 +379,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
tt.fields.headerName,
|
||||
tt.fields.audience,
|
||||
tt.fields.encryptionAlg(t),
|
||||
http.DefaultClient,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
@@ -65,12 +66,13 @@ func WithRelyingPartyOption(option rp.Option) ProviderOpts {
|
||||
}
|
||||
|
||||
// New creates a generic OAuth 2.0 provider
|
||||
func New(config *oauth2.Config, name, userEndpoint string, user func() idp.User, options ...ProviderOpts) (provider *Provider, err error) {
|
||||
func New(config *oauth2.Config, name, userEndpoint string, user func() idp.User, httpClient *http.Client, options ...ProviderOpts) (provider *Provider, err error) {
|
||||
provider = &Provider{
|
||||
name: name,
|
||||
userEndpoint: userEndpoint,
|
||||
user: user,
|
||||
generateVerifier: oauth2.GenerateVerifier,
|
||||
options: []rp.Option{rp.WithHTTPClient(httpClient)},
|
||||
}
|
||||
for _, option := range options {
|
||||
option(provider)
|
||||
|
||||
@@ -2,6 +2,7 @@ package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -70,7 +71,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, tt.fields.options...)
|
||||
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, http.DefaultClient, tt.fields.options...)
|
||||
r.NoError(err)
|
||||
provider.generateVerifier = func() string {
|
||||
return "pkceOAuthVerifier"
|
||||
@@ -170,7 +171,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, tt.fields.options...)
|
||||
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, http.DefaultClient, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
a.Equal(tt.want.name, provider.Name())
|
||||
|
||||
@@ -238,7 +238,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
tt.fields.httpMock("https://oauth2.com")
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper)
|
||||
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, http.DefaultClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
|
||||
@@ -2,6 +2,7 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
@@ -101,11 +102,12 @@ var DefaultMapper UserInfoMapper = func(info *oidc.UserInfo) idp.User {
|
||||
}
|
||||
|
||||
// New creates a generic OIDC provider
|
||||
func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) {
|
||||
func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, client *http.Client, options ...ProviderOpts) (provider *Provider, err error) {
|
||||
provider = &Provider{
|
||||
name: name,
|
||||
userInfoMapper: userInfoMapper,
|
||||
generateVerifier: oauth2.GenerateVerifier,
|
||||
options: []rp.Option{rp.WithHTTPClient(client)},
|
||||
}
|
||||
for _, option := range options {
|
||||
option(provider)
|
||||
|
||||
@@ -2,6 +2,7 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/h2non/gock"
|
||||
@@ -88,7 +89,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...)
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, http.DefaultClient, tt.fields.opts...)
|
||||
r.NoError(err)
|
||||
provider.generateVerifier = func() string {
|
||||
return "pkceOAuthVerifier"
|
||||
@@ -208,7 +209,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
tt.fields.httpMock(tt.fields.issuer)
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...)
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, http.DefaultClient, tt.fields.opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
a.Equal(tt.want.name, provider.Name())
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -325,7 +326,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
tt.fields.httpMock(tt.fields.issuer)
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.opts...)
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, http.DefaultClient, tt.opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
@@ -156,6 +157,7 @@ func New(
|
||||
metadata []byte,
|
||||
certificate []byte,
|
||||
key []byte,
|
||||
httpClient *http.Client,
|
||||
options ...ProviderOpts,
|
||||
) (*Provider, error) {
|
||||
entityDescriptor, err := ParseMetadata(metadata)
|
||||
@@ -180,6 +182,7 @@ func New(
|
||||
Certificate: keyPair.Leaf,
|
||||
IDPMetadata: entityDescriptor,
|
||||
SignRequest: false,
|
||||
HTTPClient: httpClient,
|
||||
}
|
||||
provider := &Provider{
|
||||
name: name,
|
||||
|
||||
@@ -3,6 +3,7 @@ package saml
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -107,6 +108,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
tt.fields.metadata,
|
||||
tt.fields.certificate,
|
||||
tt.fields.key,
|
||||
http.DefaultClient,
|
||||
tt.fields.options...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -285,7 +287,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, tt.fields.options...)
|
||||
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, http.DefaultClient, tt.fields.options...)
|
||||
if tt.want.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
|
||||
@@ -370,7 +370,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, tt.fields.options...)
|
||||
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, http.DefaultClient, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
session, err := NewSession(provider, tt.args.requestID, tt.args.request)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type IPLookupFunc func(string) ([]builtin_net.IP, error)
|
||||
|
||||
// HostnameToIPList converts the input URL to a list [net.IP].
|
||||
// HostnameToIPList converts the input hostname to a list [net.IP].
|
||||
//
|
||||
// Returns an internal error if the lookup fails.
|
||||
// Returns an invalid argument if the lookup function is nil
|
||||
|
||||
@@ -38,7 +38,7 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel,
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := cfg.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package set
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CallURL string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func (w *Config) Validate() error {
|
||||
|
||||
@@ -48,7 +48,7 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel,
|
||||
req.Header.Set(SigningHeader, actions.ComputeSignatureHeader(time.Now(), []byte(payload), cfg.SigningKey))
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := cfg.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type Config struct {
|
||||
Method string
|
||||
Headers http.Header
|
||||
SigningKey string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func (w *Config) Validate() error {
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/riverqueue/river"
|
||||
@@ -30,6 +31,7 @@ type BackChannelLogoutWorker struct {
|
||||
config *BackChannelLogoutWorkerConfig
|
||||
now nowFunc
|
||||
idGenerator id.Generator
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Timeout implements the Timeout-function of [river.Worker].
|
||||
@@ -97,7 +99,8 @@ func (w *BackChannelLogoutWorker) sendLogoutRequest(ctx context.Context, request
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = types.SendSecurityTokenEvent(ctx, set.Config{CallURL: request.BackChannelLogoutURI}, w.channels, &LogoutTokenMessage{LogoutToken: token}, request.TriggeringEventType).WithoutTemplate(); err != nil {
|
||||
backchannelNotifCfg := set.Config{CallURL: request.BackChannelLogoutURI, Client: w.httpClient}
|
||||
if err = types.SendSecurityTokenEvent(ctx, backchannelNotifCfg, w.channels, &LogoutTokenMessage{LogoutToken: token}, request.TriggeringEventType).WithoutTemplate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.commands.BackChannelLogoutSent(ctx, request.SessionID, request.OIDCSessionID, request.Aggregate.InstanceID)
|
||||
@@ -129,6 +132,7 @@ func NewBackChannelLogoutWorker(
|
||||
config *BackChannelLogoutWorkerConfig,
|
||||
|
||||
idGenerator id.Generator,
|
||||
httpClient *http.Client,
|
||||
) *BackChannelLogoutWorker {
|
||||
return &BackChannelLogoutWorker{
|
||||
commands: commands,
|
||||
@@ -139,6 +143,7 @@ func NewBackChannelLogoutWorker(
|
||||
config: config,
|
||||
now: time.Now,
|
||||
idGenerator: idGenerator,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -439,6 +439,7 @@ func newBackChannelLogoutWorker(queries Queries, commands Commands, es *eventsto
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
eventstore: es,
|
||||
queue: queue,
|
||||
|
||||
@@ -36,6 +36,7 @@ func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email.
|
||||
Method: http.MethodPost,
|
||||
Headers: nil,
|
||||
SigningKey: config.HTTPConfig.SigningKey,
|
||||
Client: n.httpClient,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -11,33 +13,41 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/email"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
t.Parallel()
|
||||
const instId = "instance-1"
|
||||
const cryptAlg = "alg1"
|
||||
const keyId = "key1"
|
||||
const pwd = "password"
|
||||
const clientSecret = "client-secret"
|
||||
|
||||
encryptedPwd := []byte("password-encrypted")
|
||||
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
|
||||
cryptAlgMock.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(pwd, nil)
|
||||
cryptAlgMock.EXPECT().Algorithm().Return(cryptAlg)
|
||||
cryptAlgMock.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
encryptedClientSecret := []byte("client-secret-encrypted")
|
||||
|
||||
var (
|
||||
errDecryptionFailed = errors.New("decryption failed")
|
||||
errDatabaseError = errors.New("database error")
|
||||
)
|
||||
|
||||
tt := []struct {
|
||||
smtpConfig *query.SMTPConfig
|
||||
expected *email.Config
|
||||
name string
|
||||
smtpConfig *query.SMTPConfig
|
||||
smtpConfigErr error
|
||||
setupCrypto func(m *crypto.MockEncryptionAlgorithm)
|
||||
expected *email.Config
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "smtp config with plain auth (user and password)",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp1",
|
||||
Description: "smtp config with authentication (user and password)",
|
||||
Description: "smtp config with plain auth (user and password)",
|
||||
SMTPConfig: &query.SMTP{
|
||||
TLS: true,
|
||||
SenderAddress: "[email protected]",
|
||||
@@ -55,10 +65,15 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
|
||||
m.EXPECT().Algorithm().Return(cryptAlg)
|
||||
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(pwd, nil)
|
||||
},
|
||||
expected: &email.Config{
|
||||
ProviderConfig: &email.Provider{
|
||||
ID: "smtp1",
|
||||
Description: "smtp config with authentication (user and password)",
|
||||
Description: "smtp config with plain auth (user and password)",
|
||||
},
|
||||
SMTPConfig: &smtp.Config{
|
||||
SMTP: smtp.SMTP{
|
||||
@@ -76,9 +91,10 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "smtp config with user but no plain auth config (fallback to plain auth without password)",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp2",
|
||||
Description: "smtp config with authentication (user and no password)",
|
||||
Description: "smtp config with user but no plain auth config (fallback to plain auth without password)",
|
||||
SMTPConfig: &query.SMTP{
|
||||
TLS: true,
|
||||
SenderAddress: "[email protected]",
|
||||
@@ -91,7 +107,7 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
|
||||
expected: &email.Config{
|
||||
ProviderConfig: &email.Provider{
|
||||
ID: "smtp2",
|
||||
Description: "smtp config with authentication (user and no password)",
|
||||
Description: "smtp config with user but no plain auth config (fallback to plain auth without password)",
|
||||
},
|
||||
SMTPConfig: &smtp.Config{
|
||||
SMTP: smtp.SMTP{
|
||||
@@ -108,6 +124,7 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "smtp config without authentication",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp3",
|
||||
Description: "smtp config without authentication",
|
||||
@@ -135,18 +152,205 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "smtp config with xoauth2 auth and client credentials",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp4",
|
||||
Description: "smtp config with xoauth2 auth and client credentials",
|
||||
SMTPConfig: &query.SMTP{
|
||||
SenderAddress: "[email protected]",
|
||||
SenderName: "sender",
|
||||
Host: "mail.com",
|
||||
User: "mail-user",
|
||||
XOAuth2Auth: &query.XOAuth2Auth{
|
||||
TokenEndpoint: "https://token.example.com/token",
|
||||
Scopes: []string{"https://mail.example.com/.default"},
|
||||
ClientCredentials: &query.XOAuthClientCredentials{
|
||||
ClientId: "client-id",
|
||||
ClientSecret: &crypto.CryptoValue{
|
||||
Algorithm: cryptAlg,
|
||||
KeyID: keyId,
|
||||
Crypted: encryptedClientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
|
||||
m.EXPECT().Algorithm().Return(cryptAlg)
|
||||
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(clientSecret, nil)
|
||||
},
|
||||
expected: &email.Config{
|
||||
ProviderConfig: &email.Provider{
|
||||
ID: "smtp4",
|
||||
Description: "smtp config with xoauth2 auth and client credentials",
|
||||
},
|
||||
SMTPConfig: &smtp.Config{
|
||||
SMTP: smtp.SMTP{
|
||||
Host: "mail.com",
|
||||
XOAuth2Auth: &smtp.XOAuth2AuthConfig{
|
||||
User: "mail-user",
|
||||
TokenEndpoint: "https://token.example.com/token",
|
||||
Scopes: []string{"https://mail.example.com/.default"},
|
||||
ClientCredentialsAuth: &smtp.OAuth2ClientCredentials{
|
||||
ClientId: "client-id",
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
From: "[email protected]",
|
||||
FromName: "sender",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "smtp config with xoauth2 auth without client credentials",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp5",
|
||||
Description: "smtp config with xoauth2 auth without client credentials",
|
||||
SMTPConfig: &query.SMTP{
|
||||
SenderAddress: "[email protected]",
|
||||
SenderName: "sender",
|
||||
Host: "mail.com",
|
||||
User: "mail-user",
|
||||
XOAuth2Auth: &query.XOAuth2Auth{
|
||||
TokenEndpoint: "https://token.example.com/token",
|
||||
Scopes: []string{"scope1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &email.Config{
|
||||
ProviderConfig: &email.Provider{
|
||||
ID: "smtp5",
|
||||
Description: "smtp config with xoauth2 auth without client credentials",
|
||||
},
|
||||
SMTPConfig: &smtp.Config{
|
||||
SMTP: smtp.SMTP{
|
||||
Host: "mail.com",
|
||||
XOAuth2Auth: &smtp.XOAuth2AuthConfig{
|
||||
User: "mail-user",
|
||||
TokenEndpoint: "https://token.example.com/token",
|
||||
Scopes: []string{"scope1"},
|
||||
},
|
||||
},
|
||||
From: "[email protected]",
|
||||
FromName: "sender",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plain auth password decryption error",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp6",
|
||||
Description: "plain auth password decryption error",
|
||||
SMTPConfig: &query.SMTP{
|
||||
Host: "mail.com",
|
||||
User: "mail-user",
|
||||
PlainAuth: &query.PlainAuth{
|
||||
Password: &crypto.CryptoValue{
|
||||
Algorithm: cryptAlg,
|
||||
KeyID: keyId,
|
||||
Crypted: encryptedPwd,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
|
||||
m.EXPECT().Algorithm().Return(cryptAlg)
|
||||
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
|
||||
},
|
||||
expectedErr: errDecryptionFailed,
|
||||
},
|
||||
{
|
||||
name: "xoauth2 client credentials decryption error",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp7",
|
||||
Description: "xoauth2 client credentials decryption error",
|
||||
SMTPConfig: &query.SMTP{
|
||||
Host: "mail.com",
|
||||
User: "mail-user",
|
||||
XOAuth2Auth: &query.XOAuth2Auth{
|
||||
TokenEndpoint: "https://token.example.com/token",
|
||||
ClientCredentials: &query.XOAuthClientCredentials{
|
||||
ClientId: "client-id",
|
||||
ClientSecret: &crypto.CryptoValue{
|
||||
CryptoType: 0,
|
||||
Algorithm: cryptAlg,
|
||||
KeyID: keyId,
|
||||
Crypted: encryptedClientSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
|
||||
m.EXPECT().Algorithm().Return(cryptAlg)
|
||||
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
|
||||
},
|
||||
expectedErr: errDecryptionFailed,
|
||||
},
|
||||
{
|
||||
name: "http webhook config",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "http1",
|
||||
Description: "http webhook config",
|
||||
HTTPConfig: &query.HTTP{
|
||||
Endpoint: "https://example.com/notify",
|
||||
SigningKey: "my-signing-key",
|
||||
},
|
||||
},
|
||||
expected: &email.Config{
|
||||
ProviderConfig: &email.Provider{
|
||||
ID: "http1",
|
||||
Description: "http webhook config",
|
||||
},
|
||||
WebhookConfig: &webhook.Config{
|
||||
CallURL: "https://example.com/notify",
|
||||
Method: http.MethodPost,
|
||||
SigningKey: "my-signing-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "neither smtp nor http config set",
|
||||
smtpConfig: &query.SMTPConfig{
|
||||
ID: "smtp8",
|
||||
Description: "neither smtp nor http config set",
|
||||
},
|
||||
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
|
||||
},
|
||||
{
|
||||
name: "SMTPConfigActive query error",
|
||||
smtpConfigErr: errDatabaseError,
|
||||
expectedErr: errDatabaseError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.smtpConfig.Description, func(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
ctx := authz.NewMockContext(instId, "org-1", "user-1")
|
||||
|
||||
queryMock := mock.NewMockQueries(ctrl)
|
||||
queryMock.EXPECT().SMTPConfigActive(gomock.Any(), instId).Return(tc.smtpConfig, nil)
|
||||
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
|
||||
if tc.setupCrypto != nil {
|
||||
tc.setupCrypto(cryptAlgMock)
|
||||
}
|
||||
|
||||
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, cryptAlgMock, nil)
|
||||
queryMock := mock.NewMockQueries(ctrl)
|
||||
queryMock.EXPECT().SMTPConfigActive(gomock.Any(), instId).Return(tc.smtpConfig, tc.smtpConfigErr)
|
||||
|
||||
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, cryptAlgMock, nil, nil)
|
||||
cfg, err := notificationQueries.GetActiveEmailConfig(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.ErrorIs(t, err, tc.expectedErr)
|
||||
if tc.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.EqualValues(t, tc.expected, cfg)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Conf
|
||||
Method: http.MethodPost,
|
||||
Headers: nil,
|
||||
SigningKey: config.HTTPConfig.SigningKey,
|
||||
Client: n.httpClient,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/sms"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
|
||||
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestNotificationQueries_GetActiveSMSConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const instId = "instance-1"
|
||||
const cryptAlg = "alg1"
|
||||
const keyId = "key1"
|
||||
const token = "twilio-token"
|
||||
|
||||
encryptedToken := []byte("twilio-token-encrypted")
|
||||
|
||||
var (
|
||||
errDecryptionFailed = errors.New("decryption failed")
|
||||
errDatabaseError = errors.New("database error")
|
||||
)
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
smsConfig *query.SMSConfig
|
||||
smsConfigErr error
|
||||
setupCrypto func(m *crypto.MockEncryptionAlgorithm)
|
||||
expected *sms.Config
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "twilio config with token",
|
||||
smsConfig: &query.SMSConfig{
|
||||
ID: "twilio1",
|
||||
Description: "twilio config with token",
|
||||
TwilioConfig: &query.Twilio{
|
||||
SID: "sid-1",
|
||||
SenderNumber: "+10005550100",
|
||||
VerifyServiceSID: "verify-sid-1",
|
||||
Token: &crypto.CryptoValue{
|
||||
Algorithm: cryptAlg,
|
||||
KeyID: keyId,
|
||||
Crypted: encryptedToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
|
||||
m.EXPECT().Algorithm().Return(cryptAlg)
|
||||
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(token, nil)
|
||||
},
|
||||
expected: &sms.Config{
|
||||
ProviderConfig: &sms.Provider{
|
||||
ID: "twilio1",
|
||||
Description: "twilio config with token",
|
||||
},
|
||||
TwilioConfig: &twilio.Config{
|
||||
SID: "sid-1",
|
||||
Token: token,
|
||||
SenderNumber: "+10005550100",
|
||||
VerifyServiceSID: "verify-sid-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "twilio config with nil token",
|
||||
smsConfig: &query.SMSConfig{
|
||||
ID: "twilio2",
|
||||
Description: "twilio config with nil token",
|
||||
TwilioConfig: &query.Twilio{
|
||||
SID: "sid-2",
|
||||
SenderNumber: "+10005550100",
|
||||
},
|
||||
},
|
||||
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
|
||||
},
|
||||
{
|
||||
name: "twilio token decryption error",
|
||||
smsConfig: &query.SMSConfig{
|
||||
ID: "twilio3",
|
||||
Description: "twilio token decryption error",
|
||||
TwilioConfig: &query.Twilio{
|
||||
SID: "sid-3",
|
||||
SenderNumber: "+10005550100",
|
||||
Token: &crypto.CryptoValue{
|
||||
Algorithm: cryptAlg,
|
||||
KeyID: keyId,
|
||||
Crypted: encryptedToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
|
||||
m.EXPECT().Algorithm().Return(cryptAlg)
|
||||
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
|
||||
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
|
||||
},
|
||||
expectedErr: errDecryptionFailed,
|
||||
},
|
||||
{
|
||||
name: "http webhook config",
|
||||
smsConfig: &query.SMSConfig{
|
||||
ID: "http1",
|
||||
Description: "http webhook config",
|
||||
HTTPConfig: &query.HTTP{
|
||||
Endpoint: "https://example.com/sms",
|
||||
SigningKey: "my-signing-key",
|
||||
},
|
||||
},
|
||||
expected: &sms.Config{
|
||||
ProviderConfig: &sms.Provider{
|
||||
ID: "http1",
|
||||
Description: "http webhook config",
|
||||
},
|
||||
WebhookConfig: &webhook.Config{
|
||||
CallURL: "https://example.com/sms",
|
||||
Method: http.MethodPost,
|
||||
Headers: nil,
|
||||
SigningKey: "my-signing-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "neither twilio nor http config set",
|
||||
smsConfig: &query.SMSConfig{
|
||||
ID: "sms1",
|
||||
Description: "neither twilio nor http config set",
|
||||
},
|
||||
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
|
||||
},
|
||||
{
|
||||
name: "SMSProviderConfigActive query error",
|
||||
smsConfigErr: errDatabaseError,
|
||||
expectedErr: errDatabaseError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
ctx := authz.NewMockContext(instId, "org-1", "user-1")
|
||||
|
||||
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
|
||||
if tc.setupCrypto != nil {
|
||||
tc.setupCrypto(cryptAlgMock)
|
||||
}
|
||||
|
||||
queryMock := mock.NewMockQueries(ctrl)
|
||||
queryMock.EXPECT().SMSProviderConfigActive(gomock.Any(), instId).Return(tc.smsConfig, tc.smsConfigErr)
|
||||
|
||||
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, nil, cryptAlgMock, nil)
|
||||
cfg, err := notificationQueries.GetActiveSMSConfig(ctx)
|
||||
|
||||
assert.ErrorIs(t, err, tc.expectedErr)
|
||||
if tc.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.EqualValues(t, tc.expected, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -433,6 +433,7 @@ func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock.
|
||||
f.userDataCrypto,
|
||||
smtpAlg,
|
||||
f.SMSTokenCrypto,
|
||||
nil,
|
||||
),
|
||||
channels: ¬ificationChannels{
|
||||
Chain: *senders.ChainChannels(channel),
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"golang.org/x/text/language"
|
||||
@@ -44,6 +45,7 @@ type NotificationQueries struct {
|
||||
UserDataCrypto crypto.EncryptionAlgorithm
|
||||
SMTPPasswordCrypto crypto.EncryptionAlgorithm
|
||||
SMSTokenCrypto crypto.EncryptionAlgorithm
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewNotificationQueries(
|
||||
@@ -56,6 +58,7 @@ func NewNotificationQueries(
|
||||
userDataCrypto crypto.EncryptionAlgorithm,
|
||||
smtpPasswordCrypto crypto.EncryptionAlgorithm,
|
||||
smsTokenCrypto crypto.EncryptionAlgorithm,
|
||||
httpClient *http.Client,
|
||||
) *NotificationQueries {
|
||||
return &NotificationQueries{
|
||||
Queries: baseQueries,
|
||||
@@ -67,5 +70,6 @@ func NewNotificationQueries(
|
||||
UserDataCrypto: userDataCrypto,
|
||||
SMTPPasswordCrypto: smtpPasswordCrypto,
|
||||
SMSTokenCrypto: smsTokenCrypto,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (u *quotaNotifier) reduceNotificationDue(event eventstore.Event) (*handler.
|
||||
if alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost}, u.channels, e, e.Type()).WithoutTemplate()
|
||||
err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost, Client: u.queries.httpClient}, u.channels, e, e.Type()).WithoutTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ func (t *telemetryPusher) pushMilestone(ctx context.Context, e *milestone.Reache
|
||||
CallURL: endpoint,
|
||||
Method: http.MethodPost,
|
||||
Headers: t.cfg.Headers,
|
||||
Client: t.queries.httpClient,
|
||||
},
|
||||
t.channels,
|
||||
&struct {
|
||||
|
||||
@@ -1972,6 +1972,7 @@ func newUserNotifierLegacy(t *testing.T, ctrl *gomock.Controller, queries *mock.
|
||||
f.userDataCrypto,
|
||||
smtpAlg,
|
||||
f.SMSTokenCrypto,
|
||||
nil,
|
||||
),
|
||||
otpEmailTmpl: func(origin *url.URL) string {
|
||||
return origin.String() + defaultOTPEmailTemplate
|
||||
|
||||
@@ -1936,6 +1936,7 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu
|
||||
f.userDataCrypto,
|
||||
smtpAlg,
|
||||
f.SMSTokenCrypto,
|
||||
nil,
|
||||
),
|
||||
otpEmailTmpl: func(origin *url.URL) string {
|
||||
return origin.String() + defaultOTPEmailTemplate
|
||||
|
||||
@@ -3,6 +3,7 @@ package notification
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
@@ -40,6 +41,7 @@ func Register(
|
||||
fileSystemPath string,
|
||||
userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm,
|
||||
queue *queue.Queue,
|
||||
httpClient *http.Client,
|
||||
) {
|
||||
if !notificationWorkerConfig.LegacyEnabled {
|
||||
queue.ShouldStart()
|
||||
@@ -48,7 +50,7 @@ func Register(
|
||||
// make sure the slice does not contain old values
|
||||
projections = nil
|
||||
|
||||
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
|
||||
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, httpClient)
|
||||
c := newChannels(q)
|
||||
projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig, queue))
|
||||
projections = append(projections, handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c))
|
||||
@@ -59,7 +61,16 @@ func Register(
|
||||
queue,
|
||||
backChannelLogoutWorkerConfig.MaxAttempts,
|
||||
))
|
||||
queue.AddWorkers(ctx, handlers.NewBackChannelLogoutWorker(commands, q, es, queue, c, backChannelLogoutWorkerConfig, id.SonyFlakeGenerator()))
|
||||
queue.AddWorkers(ctx, handlers.NewBackChannelLogoutWorker(
|
||||
commands,
|
||||
q,
|
||||
es,
|
||||
queue,
|
||||
c,
|
||||
backChannelLogoutWorkerConfig,
|
||||
id.SonyFlakeGenerator(),
|
||||
httpClient,
|
||||
))
|
||||
if telemetryCfg.Enabled {
|
||||
projections = append(projections, handlers.NewTelemetryPusher(ctx, telemetryCfg, projection.ApplyCustomConfig(telemetryHandlerCustomConfig), commands, q, c))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user