Compare commits

...

11 Commits

Author SHA1 Message Date
d667bb03f5 Merge pull request 'Mise en place de cache local au niveau du serveur pour améliorer les temps de traitement des requêtes' (#26) from benchmark into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #26
2024-05-28 16:52:34 +02:00
3a9fde9bc9 feat: improve perf by caching proxy and layers locally
Some checks are pending
Cadoles/bouncer/pipeline/head This commit looks good
Cadoles/bouncer/pipeline/pr-develop Build started...
2024-05-28 16:45:15 +02:00
42dab5797a chore(doc): fix typo
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-05-28 11:16:15 +02:00
132bf1e642 feat(authn-oidc): allow for dynamic post-logout redirection
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-05-24 17:01:06 +02:00
26a9ad0e2e feat(authn-oidc): match login callback/logout urls with query string by default
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-05-24 15:28:21 +02:00
3e5dd446cb feat(authn-oidc): use relative redirection to prevent internal/public host mixing 2024-05-24 15:27:43 +02:00
d5c846a9ce fix(docker): format default config durations
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-05-24 14:52:31 +02:00
82c93d3f1e feat(config): interpolate recursively in interpolated map
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-05-24 12:49:03 +02:00
544326a4b7 feat(authn-oidc): use full urls for login callback/logout options 2024-05-23 17:41:36 +02:00
499bb3696d fix(authn-network): handles r.RemoteAddr without port
Some checks are pending
Cadoles/bouncer/pipeline/head This commit looks good
Cadoles/bouncer/pipeline/pr-authn-oidc-redirect-url Build started...
2024-05-22 15:24:40 +02:00
572093536a feat(authn): do not allow additional options
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
2024-05-22 14:41:54 +02:00
37 changed files with 1054 additions and 152 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@
/data /data
/out /out
.dockerconfigjson .dockerconfigjson
*.prof
proxy.test

View File

@ -28,7 +28,9 @@ RUN /src/dist/bouncer_linux_amd64_v1/bouncer -c '' config dump > /src/dist/bounc
&& yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.redis.adresses = ["redis:6379"]' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.writeTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.redis.writeTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.readTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \ && yq -i '.redis.readTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.redis.dialTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml && yq -i '.redis.dialTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.bootstrap.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml \
&& yq -i '.integrations.kubernetes.lockTimeout = "30s"' /src/dist/bouncer_linux_amd64_v1/config.yml
FROM reg.cadoles.com/proxy_cache/library/alpine:3.19.1 AS RUNTIME FROM reg.cadoles.com/proxy_cache/library/alpine:3.19.1 AS RUNTIME
@ -46,6 +48,7 @@ RUN ln -s /usr/share/bouncer/bin/bouncer /usr/local/bin/bouncer
EXPOSE 8080 EXPOSE 8080
EXPOSE 8081 EXPOSE 8081
EXPOSE 8082
RUN adduser -D -H bouncer RUN adduser -D -H bouncer

View File

@ -130,6 +130,13 @@ tools/grafterm/bin/grafterm:
mkdir -p tools/grafterm/bin mkdir -p tools/grafterm/bin
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0 GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
bench:
go test -bench=. -run '^$$' -count=10 ./...
tools/benchstat/bin/benchstat:
mkdir -p tools/benchstat/bin
GOBIN=$(PWD)/tools/benchstat/bin go install golang.org/x/perf/cmd/benchstat@latest
full-version: full-version:
@echo $(FULL_VERSION) @echo $(FULL_VERSION)

View File

@ -55,7 +55,7 @@ Par défaut ce serveur écoute sur le port 8082. Il est possible de modifier l'a
bouncer admin proxy update --proxy-name my-proxy --proxy-enabled bouncer admin proxy update --proxy-name my-proxy --proxy-enabled
``` ```
3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut 3. À ce stade, vous devriez pouvoir afficher la page du serveur `dummy` en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous travaillez avec une instance Bouncer locale avec la configuration par défaut
4. Créer un layer de type `authn-oidc` pour notre nouveau proxy 4. Créer un layer de type `authn-oidc` pour notre nouveau proxy

View File

@ -0,0 +1,31 @@
# Analyser les performances de Bouncer
1. Lancer un benchmark du proxy
```shell
go test -bench=. -run '^$' -count=5 -cpuprofile bench_proxy.prof ./internal/proxy
```
2. Visualiser les temps d'exécution
```shell
go tool pprof -web bench_proxy.prof
```
3. Comparer les performances d'une exécution à l'autre
```shell
# Lancer un premier benchmark
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_before.txt
# Faire des modifications sur les sources
# Lancer un second benchmark
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_after.txt
# Installer l'outil benchstat
make tools/benchstat/bin/benchstat
# Comparer les rapports
tools/benchstat/bin/benchstat bench_before.txt bench_after.txt
```

1
go.mod
View File

@ -93,6 +93,7 @@ require (
go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/net v0.19.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect

4
go.sum
View File

@ -405,8 +405,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -65,7 +65,7 @@ func (s *Server) bootstrapProxies(ctx context.Context) error {
for layerName, layerConfig := range proxyConfig.Layers { for layerName, layerConfig := range proxyConfig.Layers {
layerType := store.LayerType(layerConfig.Type) layerType := store.LayerType(layerConfig.Type)
layerOptions := store.LayerOptions(layerConfig.Options) layerOptions := store.LayerOptions(layerConfig.Options.Data)
if _, err := layerRepo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions); err != nil { if _, err := layerRepo.CreateLayer(ctx, proxyName, layerName, layerType, layerOptions); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -109,7 +109,7 @@ func (s *Server) validateBootstrap(ctx context.Context) error {
} }
rawOptions := func(opts config.InterpolatedMap) map[string]any { rawOptions := func(opts config.InterpolatedMap) map[string]any {
return opts return opts.Data
}(layerConf.Options) }(layerConf.Options)
if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil { if err := schema.Validate(ctx, layerOptionsSchema, rawOptions); err != nil {

6
internal/cache/cache.go vendored Normal file
View File

@ -0,0 +1,6 @@
package cache
type Cache[K comparable, V any] interface {
Get(key K) (V, bool)
Set(key K, value V)
}

34
internal/cache/memory/cache.go vendored Normal file
View File

@ -0,0 +1,34 @@
package memory
import (
"sync"
cache "forge.cadoles.com/cadoles/bouncer/internal/cache"
)
type Cache[K comparable, V any] struct {
store *sync.Map
}
// Get implements cache.Cache.
func (c *Cache[K, V]) Get(key K) (V, bool) {
raw, exists := c.store.Load(key)
if !exists {
return *new(V), false
}
return raw.(V), exists
}
// Set implements cache.Cache.
func (c *Cache[K, V]) Set(key K, value V) {
c.store.Store(key, value)
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
store: new(sync.Map),
}
}
var _ cache.Cache[string, bool] = &Cache[string, bool]{}

39
internal/cache/ttl/cache.go vendored Normal file
View File

@ -0,0 +1,39 @@
package ttl
import (
"time"
cache "forge.cadoles.com/cadoles/bouncer/internal/cache"
)
type Cache[K comparable, V any] struct {
timestamps cache.Cache[K, time.Time]
values cache.Cache[K, V]
ttl time.Duration
}
// Get implements cache.Cache.
func (c *Cache[K, V]) Get(key K) (V, bool) {
timestamp, exists := c.timestamps.Get(key)
if !exists || timestamp.Add(c.ttl).Before(time.Now()) {
return *new(V), false
}
return c.values.Get(key)
}
// Set implements cache.Cache.
func (c *Cache[K, V]) Set(key K, value V) {
c.timestamps.Set(key, time.Now())
c.values.Set(key, value)
}
func NewCache[K comparable, V any](values cache.Cache[K, V], timestamps cache.Cache[K, time.Time], ttl time.Duration) *Cache[K, V] {
return &Cache[K, V]{
values: values,
timestamps: timestamps,
ttl: ttl,
}
}
var _ cache.Cache[string, bool] = &Cache[string, bool]{}

39
internal/cache/ttl/cache_test.go vendored Normal file
View File

@ -0,0 +1,39 @@
package ttl
import (
"testing"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
)
func TestCache(t *testing.T) {
cache := NewCache(
memory.NewCache[string, int](),
memory.NewCache[string, time.Time](),
time.Second,
)
key := "foo"
if _, exists := cache.Get(key); exists {
t.Errorf("cache.Get(\"%s\"): should not exists", key)
}
cache.Set(key, 1)
value, exists := cache.Get(key)
if !exists {
t.Errorf("cache.Get(\"%s\"): should exists", key)
}
if e, g := 1, value; e != g {
t.Errorf("cache.Get(\"%s\"): expected '%v', got '%v'", key, e, g)
}
time.Sleep(time.Second)
if _, exists := cache.Get("foo"); exists {
t.Errorf("cache.Get(\"%s\"): should not exists", key)
}
}

View File

@ -3,6 +3,7 @@ package proxy
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"forge.cadoles.com/cadoles/bouncer/internal/command/common" "forge.cadoles.com/cadoles/bouncer/internal/command/common"
"forge.cadoles.com/cadoles/bouncer/internal/proxy" "forge.cadoles.com/cadoles/bouncer/internal/proxy"
@ -45,6 +46,7 @@ func RunCommand() *cli.Command {
proxy.WithServerConfig(conf.Proxy), proxy.WithServerConfig(conf.Proxy),
proxy.WithRedisConfig(conf.Redis), proxy.WithRedisConfig(conf.Redis),
proxy.WithDirectorLayers(layers...), proxy.WithDirectorLayers(layers...),
proxy.WithDirectorCacheTTL(time.Duration(conf.Proxy.Cache.TTL)),
) )
addrs, srvErrs := srv.Start(ctx.Context) addrs, srvErrs := srv.Start(ctx.Context)

View File

@ -101,33 +101,66 @@ func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
type InterpolatedMap map[string]interface{} type InterpolatedMap struct {
Data map[string]any
getEnv func(string) string
}
func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error { func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error {
var data map[string]interface{} var data map[string]any
if err := value.Decode(&data); err != nil { if err := value.Decode(&data); err != nil {
return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line)
} }
for key, value := range data { if im.getEnv == nil {
strVal, ok := value.(string) im.getEnv = os.Getenv
if !ok {
continue
}
if match := reVar.FindStringSubmatch(strVal); len(match) > 0 {
strVal = os.Getenv(match[1])
}
data[key] = strVal
} }
*im = data interpolated, err := im.interpolateRecursive(data)
if err != nil {
return errors.WithStack(err)
}
im.Data = interpolated.(map[string]any)
return nil return nil
} }
func (im *InterpolatedMap) interpolateRecursive(data any) (any, error) {
switch typ := data.(type) {
case map[string]any:
for key, value := range typ {
value, err := im.interpolateRecursive(value)
if err != nil {
return nil, errors.WithStack(err)
}
typ[key] = value
}
case string:
value, err := envsubst.Eval(typ, im.getEnv)
if err != nil {
return nil, errors.WithStack(err)
}
data = value
case []any:
for idx := range typ {
value, err := im.interpolateRecursive(typ[idx])
if err != nil {
return nil, errors.WithStack(err)
}
typ[idx] = value
}
}
return data, nil
}
type InterpolatedStringSlice []string type InterpolatedStringSlice []string
func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error { func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error {

View File

@ -0,0 +1,82 @@
package config
import (
"fmt"
"os"
"testing"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
func TestInterpolatedMap(t *testing.T) {
type testCase struct {
Path string
Env map[string]string
Assert func(t *testing.T, parsed InterpolatedMap)
}
testCases := []testCase{
{
Path: "testdata/environment/interpolated-map-1.yml",
Env: map[string]string{
"TEST_PROP1": "foo",
"TEST_SUB_PROP1": "bar",
"TEST_SUB2_PROP1": "baz",
},
Assert: func(t *testing.T, parsed InterpolatedMap) {
if e, g := "foo", parsed.Data["prop1"]; e != g {
t.Errorf("parsed.Data[\"prop1\"]: expected '%v', got '%v'", e, g)
}
if e, g := "bar", parsed.Data["sub"].(map[string]any)["subProp1"]; e != g {
t.Errorf("parsed.Data[\"sub\"][\"subProp1\"]: expected '%v', got '%v'", e, g)
}
if e, g := "baz", parsed.Data["sub2"].(map[string]any)["sub2Prop1"].([]any)[0]; e != g {
t.Errorf("parsed.Data[\"sub2\"][\"sub2Prop1\"][0]: expected '%v', got '%v'", e, g)
}
if e, g := "test", parsed.Data["sub2"].(map[string]any)["sub2Prop1"].([]any)[1]; e != g {
t.Errorf("parsed.Data[\"sub2\"][\"sub2Prop1\"][1]: expected '%v', got '%v'", e, g)
}
},
},
{
Path: "testdata/environment/interpolated-map-2.yml",
Env: map[string]string{
"BAR": "bar",
},
Assert: func(t *testing.T, parsed InterpolatedMap) {
if e, g := "http://bar", parsed.Data["foo"]; e != g {
t.Errorf("parsed.Data[\"foo\"]: expected '%v', got '%v'", e, g)
}
},
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("Case #%d", idx), func(t *testing.T) {
data, err := os.ReadFile(tc.Path)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
var interpolatedMap InterpolatedMap
if tc.Env != nil {
interpolatedMap.getEnv = func(key string) string {
return tc.Env[key]
}
}
if err := yaml.Unmarshal(data, &interpolatedMap); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if tc.Assert != nil {
tc.Assert(t, interpolatedMap)
}
})
}
}

View File

@ -15,6 +15,12 @@ func NewDefaultLayersConfig() LayersConfig {
}, },
Authn: AuthnLayerConfig{ Authn: AuthnLayerConfig{
TemplateDir: "./layers/authn/templates", TemplateDir: "./layers/authn/templates",
OIDC: AuthnOIDCLayerConfig{
HTTPClient: AuthnOIDCHTTPClientConfig{
TransportConfig: NewDefaultTransportConfig(),
Timeout: NewInterpolatedDuration(10 * time.Second),
},
},
}, },
} }
} }
@ -25,5 +31,15 @@ type QueueLayerConfig struct {
} }
type AuthnLayerConfig struct { type AuthnLayerConfig struct {
TemplateDir InterpolatedString `yaml:"templateDir"` TemplateDir InterpolatedString `yaml:"templateDir"`
OIDC AuthnOIDCLayerConfig `yaml:"oidc"`
}
type AuthnOIDCLayerConfig struct {
HTTPClient AuthnOIDCHTTPClientConfig `yaml:"httpClient"`
}
type AuthnOIDCHTTPClientConfig struct {
TransportConfig
Timeout *InterpolatedDuration `yaml:"timeout"`
} }

View File

@ -17,9 +17,9 @@ func (c *BasicAuthConfig) CredentialsMap() map[string]string {
return map[string]string{} return map[string]string{}
} }
credentials := make(map[string]string, len(*c.Credentials)) credentials := make(map[string]string, len(c.Credentials.Data))
for k, v := range *c.Credentials { for k, v := range c.Credentials.Data {
credentials[k] = fmt.Sprintf("%v", v) credentials[k] = fmt.Sprintf("%v", v)
} }

View File

@ -1,6 +1,10 @@
package config package config
import "time" import (
"crypto/tls"
"net/http"
"time"
)
type ProxyServerConfig struct { type ProxyServerConfig struct {
HTTP HTTPConfig `yaml:"http"` HTTP HTTPConfig `yaml:"http"`
@ -8,6 +12,18 @@ type ProxyServerConfig struct {
Transport TransportConfig `yaml:"transport"` Transport TransportConfig `yaml:"transport"`
Dial DialConfig `yaml:"dial"` Dial DialConfig `yaml:"dial"`
Sentry SentryConfig `yaml:"sentry"` Sentry SentryConfig `yaml:"sentry"`
Cache CacheConfig `yaml:"cache"`
}
func NewDefaultProxyServerConfig() ProxyServerConfig {
return ProxyServerConfig{
HTTP: NewHTTPConfig("0.0.0.0", 8080),
Metrics: NewDefaultMetricsConfig(),
Transport: NewDefaultTransportConfig(),
Dial: NewDefaultDialConfig(),
Sentry: NewDefaultSentryConfig(),
Cache: NewDefaultCacheConfig(),
}
} }
// See https://pkg.go.dev/net/http#Transport // See https://pkg.go.dev/net/http#Transport
@ -25,15 +41,51 @@ type TransportConfig struct {
WriteBufferSize InterpolatedInt `yaml:"writeBufferSize"` WriteBufferSize InterpolatedInt `yaml:"writeBufferSize"`
ReadBufferSize InterpolatedInt `yaml:"readBufferSize"` ReadBufferSize InterpolatedInt `yaml:"readBufferSize"`
MaxResponseHeaderBytes InterpolatedInt `yaml:"maxResponseHeaderBytes"` MaxResponseHeaderBytes InterpolatedInt `yaml:"maxResponseHeaderBytes"`
InsecureSkipVerify InterpolatedBool `yaml:"insecureSkipVerify"`
} }
func NewDefaultProxyServerConfig() ProxyServerConfig { func (c TransportConfig) AsTransport() *http.Transport {
return ProxyServerConfig{ httpTransport := http.DefaultTransport.(*http.Transport).Clone()
HTTP: NewHTTPConfig("0.0.0.0", 8080),
Metrics: NewDefaultMetricsConfig(), httpTransport.Proxy = http.ProxyFromEnvironment
Transport: NewDefaultTransportConfig(), httpTransport.ForceAttemptHTTP2 = bool(c.ForceAttemptHTTP2)
Dial: NewDefaultDialConfig(), httpTransport.MaxIdleConns = int(c.MaxIdleConns)
Sentry: NewDefaultSentryConfig(), httpTransport.MaxIdleConnsPerHost = int(c.MaxIdleConnsPerHost)
httpTransport.MaxConnsPerHost = int(c.MaxConnsPerHost)
httpTransport.IdleConnTimeout = time.Duration(*c.IdleConnTimeout)
httpTransport.TLSHandshakeTimeout = time.Duration(*c.TLSHandshakeTimeout)
httpTransport.ExpectContinueTimeout = time.Duration(*c.ExpectContinueTimeout)
httpTransport.DisableKeepAlives = bool(c.DisableKeepAlives)
httpTransport.DisableCompression = bool(c.DisableCompression)
httpTransport.ResponseHeaderTimeout = time.Duration(*c.ResponseHeaderTimeout)
httpTransport.WriteBufferSize = int(c.WriteBufferSize)
httpTransport.ReadBufferSize = int(c.ReadBufferSize)
httpTransport.MaxResponseHeaderBytes = int64(c.MaxResponseHeaderBytes)
if httpTransport.TLSClientConfig == nil {
httpTransport.TLSClientConfig = &tls.Config{}
}
httpTransport.TLSClientConfig.InsecureSkipVerify = bool(c.InsecureSkipVerify)
return httpTransport
}
func NewDefaultTransportConfig() TransportConfig {
return TransportConfig{
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second),
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
DisableCompression: false,
DisableKeepAlives: false,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
MaxResponseHeaderBytes: 0,
InsecureSkipVerify: false,
} }
} }
@ -54,20 +106,12 @@ func NewDefaultDialConfig() DialConfig {
} }
} }
func NewDefaultTransportConfig() TransportConfig { type CacheConfig struct {
return TransportConfig{ TTL InterpolatedDuration `yaml:"ttl"`
ForceAttemptHTTP2: true, }
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100, func NewDefaultCacheConfig() CacheConfig {
MaxConnsPerHost: 100, return CacheConfig{
IdleConnTimeout: NewInterpolatedDuration(90 * time.Second), TTL: *NewInterpolatedDuration(time.Second * 30),
TLSHandshakeTimeout: NewInterpolatedDuration(10 * time.Second),
ExpectContinueTimeout: NewInterpolatedDuration(1 * time.Second),
ResponseHeaderTimeout: NewInterpolatedDuration(10 * time.Second),
DisableCompression: false,
DisableKeepAlives: false,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
MaxResponseHeaderBytes: 0,
} }
} }

View File

@ -0,0 +1,6 @@
prop1: "${TEST_PROP1}"
prop2: 1
sub:
subProp1: "${TEST_SUB_PROP1}"
sub2:
sub2Prop1: ["${TEST_SUB2_PROP1}", "test"]

View File

@ -0,0 +1 @@
foo: http://${BAR}

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard" "forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/cache"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -17,6 +18,9 @@ type Director struct {
proxyRepository store.ProxyRepository proxyRepository store.ProxyRepository
layerRepository store.LayerRepository layerRepository store.LayerRepository
layerRegistry *LayerRegistry layerRegistry *LayerRegistry
proxyCache cache.Cache[string, []*store.Proxy]
layerCache cache.Cache[string, []*store.Layer]
} }
func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) { func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
@ -88,7 +92,14 @@ MAIN:
return r, nil return r, nil
} }
const proxiesCacheKey = "proxies"
func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) { func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
proxies, exists := d.proxyCache.Get(proxiesCacheKey)
if exists {
return proxies, nil
}
headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true)) headers, err := d.proxyRepository.QueryProxy(ctx, store.WithProxyQueryEnabled(true))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
@ -96,7 +107,7 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
sort.Sort(store.ByProxyWeight(headers)) sort.Sort(store.ByProxyWeight(headers))
proxies := make([]*store.Proxy, 0, len(headers)) proxies = make([]*store.Proxy, 0, len(headers))
for _, h := range headers { for _, h := range headers {
if !h.Enabled { if !h.Enabled {
@ -111,10 +122,19 @@ func (d *Director) getProxies(ctx context.Context) ([]*store.Proxy, error) {
proxies = append(proxies, proxy) proxies = append(proxies, proxy)
} }
d.proxyCache.Set(proxiesCacheKey, proxies)
return proxies, nil return proxies, nil
} }
func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) { func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]*store.Layer, error) {
cacheKey := "layers-" + string(proxyName)
layers, exists := d.layerCache.Get(cacheKey)
if exists {
return layers, nil
}
headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true)) headers, err := d.layerRepository.QueryLayers(ctx, proxyName, store.WithLayerQueryEnabled(true))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
@ -122,7 +142,7 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
sort.Sort(store.ByLayerWeight(headers)) sort.Sort(store.ByLayerWeight(headers))
layers := make([]*store.Layer, 0, len(headers)) layers = make([]*store.Layer, 0, len(headers))
for _, h := range headers { for _, h := range headers {
if !h.Enabled { if !h.Enabled {
@ -137,6 +157,8 @@ func (d *Director) getLayers(ctx context.Context, proxyName store.ProxyName) ([]
layers = append(layers, layer) layers = append(layers, layer)
} }
d.layerCache.Set(cacheKey, layers)
return layers, nil return layers, nil
} }
@ -240,8 +262,16 @@ func (d *Director) Middleware() proxy.Middleware {
} }
} }
func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, layers ...Layer) *Director { func New(proxyRepository store.ProxyRepository, layerRepository store.LayerRepository, funcs ...OptionFunc) *Director {
registry := NewLayerRegistry(layers...) opts := NewOptions(funcs...)
return &Director{proxyRepository, layerRepository, registry} registry := NewLayerRegistry(opts.Layers...)
return &Director{
proxyRepository: proxyRepository,
layerRepository: layerRepository,
layerRegistry: registry,
proxyCache: opts.ProxyCache,
layerCache: opts.LayerCache,
}
} }

View File

@ -43,5 +43,6 @@
} }
} }
} }
} },
"additionalProperties": false
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
"net/http" "net/http"
"strings"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
@ -49,9 +50,15 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
} }
func (a *Authenticator) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) { func (a *Authenticator) matchAnyAuthorizedCIDRs(ctx context.Context, remoteHostPort string, CIDRs []string) (bool, error) {
remoteHost, _, err := net.SplitHostPort(remoteHostPort) var remoteHost string
if err != nil { if strings.Contains(remoteHostPort, ":") {
return false, errors.WithStack(err) var err error
remoteHost, _, err = net.SplitHostPort(remoteHostPort)
if err != nil {
return false, errors.WithStack(err)
}
} else {
remoteHost = remoteHostPort
} }
remoteAddr := net.ParseIP(remoteHost) remoteAddr := net.ParseIP(remoteHost)

View File

@ -0,0 +1,60 @@
package network
import (
"context"
"fmt"
"testing"
"github.com/pkg/errors"
)
func TestMatchAuthorizedCIDRs(t *testing.T) {
type testCase struct {
RemoteHostPort string
AuthorizedCIDRs []string
ExpectedResult bool
ExpectedError error
}
testCases := []testCase{
{
RemoteHostPort: "192.168.1.15",
AuthorizedCIDRs: []string{
"192.168.1.0/24",
},
ExpectedResult: true,
},
{
RemoteHostPort: "192.168.1.15:43349",
AuthorizedCIDRs: []string{
"192.168.1.0/24",
},
ExpectedResult: true,
},
{
RemoteHostPort: "192.168.1.15:43349",
AuthorizedCIDRs: []string{
"192.168.1.5/32",
},
ExpectedResult: false,
},
}
auth := Authenticator{}
ctx := context.Background()
for idx, tc := range testCases {
t.Run(fmt.Sprintf("Case #%d", idx), func(t *testing.T) {
result, err := auth.matchAnyAuthorizedCIDRs(ctx, tc.RemoteHostPort, tc.AuthorizedCIDRs)
if g, e := result, tc.ExpectedResult; e != g {
t.Errorf("result: expected '%v', got '%v'", e, g)
}
if e, g := tc.ExpectedError, err; !errors.Is(err, tc.ExpectedError) {
t.Errorf("err: expected '%v', got '%v'", e, g)
}
})
}
}

View File

@ -1,11 +1,18 @@
package oidc package oidc
import ( import (
"bytes"
"context" "context"
"crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"strings"
"text/template"
"time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
@ -17,7 +24,9 @@ import (
) )
type Authenticator struct { type Authenticator struct {
store sessions.Store store sessions.Store
httpTransport *http.Transport
httpClientTimeout time.Duration
} }
func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error { func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error {
@ -38,16 +47,30 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err)))
} }
redirectURL := a.getRedirectURL(layer.Proxy, layer.Name, originalURL, options) loginCallbackURL, err := a.getLoginCallbackURL(originalURL, layer.Proxy, layer.Name, options)
logoutURL := a.getLogoutURL(layer.Proxy, layer.Name, originalURL, options)
client, err := a.getClient(options, redirectURL.String())
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
switch r.URL.Path { client, err := a.getClient(options, loginCallbackURL.String())
case redirectURL.Path: if err != nil {
return errors.WithStack(err)
}
loginCallbackPathPattern, err := a.templatize(options.OIDC.MatchLoginCallbackPath, layer.Proxy, layer.Name)
if err != nil {
return errors.WithStack(err)
}
logoutPathPattern, err := a.templatize(options.OIDC.MatchLogoutPath, layer.Proxy, layer.Name)
if err != nil {
return errors.WithStack(err)
}
logger.Debug(ctx, "checking url", logger.F("loginCallbackPathPattern", loginCallbackPathPattern), logger.F("logoutPathPattern", logoutPathPattern))
switch {
case wildcard.Match(originalURL.Path, loginCallbackPathPattern):
if err := client.HandleCallback(w, r, sess); err != nil { if err := client.HandleCallback(w, r, sess); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -57,10 +80,23 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
metricLabelProxy: string(layer.Proxy), metricLabelProxy: string(layer.Proxy),
}).Add(1) }).Add(1)
case logoutURL.Path: case wildcard.Match(originalURL.Path, logoutPathPattern):
postLogoutRedirectURL := options.OIDC.PostLogoutRedirectURL postLogoutRedirectURL := r.URL.Query().Get("redirect")
if options.OIDC.PostLogoutRedirectURL == "" {
postLogoutRedirectURL = originalURL.Scheme + "://" + originalURL.Host if postLogoutRedirectURL != "" {
isAuthorized := slices.Contains(options.OIDC.PostLogoutRedirectURLs, postLogoutRedirectURL)
if !isAuthorized {
http.Error(w, "unauthorized post-logout redirect", http.StatusBadRequest)
return errors.WithStack(authn.ErrSkipRequest)
}
}
if postLogoutRedirectURL == "" {
if options.OIDC.PublicBaseURL != "" {
postLogoutRedirectURL = options.OIDC.PublicBaseURL
} else {
postLogoutRedirectURL = originalURL.Scheme + "://" + originalURL.Host
}
} }
if err := client.HandleLogout(w, r, sess, postLogoutRedirectURL); err != nil { if err := client.HandleLogout(w, r, sess, postLogoutRedirectURL); err != nil {
@ -80,11 +116,6 @@ func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) { func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
ctx := r.Context() ctx := r.Context()
originalURL, err := director.OriginalURL(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
options, err := fromStoreOptions(layer.Options) options, err := fromStoreOptions(layer.Options)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
@ -117,14 +148,27 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
sess.Options.SameSite = http.SameSiteDefaultMode sess.Options.SameSite = http.SameSiteDefaultMode
} }
redirectURL := a.getRedirectURL(layer.Proxy, layer.Name, originalURL, options) originalURL, err := director.OriginalURL(ctx)
client, err := a.getClient(options, redirectURL.String())
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
idToken, err := client.Authenticate(w, r, sess) loginCallbackURL, err := a.getLoginCallbackURL(originalURL, layer.Proxy, layer.Name, options)
if err != nil {
return nil, errors.WithStack(err)
}
client, err := a.getClient(options, loginCallbackURL.String())
if err != nil {
return nil, errors.WithStack(err)
}
postLoginRedirectURL, err := a.mergeURL(originalURL, originalURL.Path, options.OIDC.PublicBaseURL, true)
if err != nil {
return nil, errors.WithStack(err)
}
idToken, err := client.Authenticate(w, r, sess, postLoginRedirectURL.String())
if err != nil { if err != nil {
if errors.Is(err, ErrLoginRequired) { if errors.Is(err, ErrLoginRequired) {
metricLoginRequestsTotal.With(prometheus.Labels{ metricLoginRequestsTotal.With(prometheus.Labels{
@ -138,7 +182,7 @@ func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, lay
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
user, err := a.toUser(idToken, layer.Proxy, layer.Name, originalURL, options, sess) user, err := a.toUser(originalURL, idToken, layer.Proxy, layer.Name, options, sess)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -196,7 +240,7 @@ func (c claims) AsAttrs() map[string]any {
return attrs return attrs
} }
func (a *Authenticator) toUser(idToken *oidc.IDToken, proxyName store.ProxyName, layerName store.LayerName, originalURL *url.URL, options *LayerOptions, sess *sessions.Session) (*authn.User, error) { func (a *Authenticator) toUser(originalURL *url.URL, idToken *oidc.IDToken, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions, sess *sessions.Session) (*authn.User, error) {
var claims claims var claims claims
if err := idToken.Claims(&claims); err != nil { if err := idToken.Claims(&claims); err != nil {
@ -209,7 +253,11 @@ func (a *Authenticator) toUser(idToken *oidc.IDToken, proxyName store.ProxyName,
attrs := claims.AsAttrs() attrs := claims.AsAttrs()
logoutURL := a.getLogoutURL(proxyName, layerName, originalURL, options) logoutURL, err := a.getLogoutURL(originalURL, proxyName, layerName, options)
if err != nil {
return nil, errors.WithStack(err)
}
attrs["logout_url"] = logoutURL.String() attrs["logout_url"] = logoutURL.String()
if accessToken, exists := sess.Values[sessionKeyAccessToken]; exists && accessToken != nil { if accessToken, exists := sess.Values[sessionKeyAccessToken]; exists && accessToken != nil {
@ -229,25 +277,109 @@ func (a *Authenticator) toUser(idToken *oidc.IDToken, proxyName store.ProxyName,
return user, nil return user, nil
} }
func (a *Authenticator) getRedirectURL(proxyName store.ProxyName, layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL { func (a *Authenticator) getLoginCallbackURL(originalURL *url.URL, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) (*url.URL, error) {
return &url.URL{ path, err := a.templatize(options.OIDC.LoginCallbackPath, proxyName, layerName)
Scheme: u.Scheme, if err != nil {
Host: u.Host, return nil, errors.WithStack(err)
Path: fmt.Sprintf(options.OIDC.LoginCallbackPath, fmt.Sprintf("%s/%s", proxyName, layerName)),
} }
merged, err := a.mergeURL(originalURL, path, options.OIDC.PublicBaseURL, false)
if err != nil {
return nil, errors.WithStack(err)
}
return merged, nil
} }
func (a *Authenticator) getLogoutURL(proxyName store.ProxyName, layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL { func (a *Authenticator) getLogoutURL(originalURL *url.URL, proxyName store.ProxyName, layerName store.LayerName, options *LayerOptions) (*url.URL, error) {
return &url.URL{ path, err := a.templatize(options.OIDC.LogoutPath, proxyName, layerName)
Scheme: u.Scheme, if err != nil {
Host: u.Host, return nil, errors.WithStack(err)
Path: fmt.Sprintf(options.OIDC.LogoutPath, fmt.Sprintf("%s/%s", proxyName, layerName)),
} }
merged, err := a.mergeURL(originalURL, path, options.OIDC.PublicBaseURL, true)
if err != nil {
return nil, errors.WithStack(err)
}
return merged, nil
}
func (a *Authenticator) mergeURL(base *url.URL, path string, overlay string, withQuery bool) (*url.URL, error) {
merged := &url.URL{
Scheme: base.Scheme,
Host: base.Host,
Path: path,
}
if withQuery {
merged.RawQuery = base.RawQuery
}
if overlay != "" {
overlayURL, err := url.Parse(overlay)
if err != nil {
return nil, errors.WithStack(err)
}
merged.Scheme = overlayURL.Scheme
merged.Host = overlayURL.Host
merged.Path = overlayURL.Path + strings.TrimPrefix(path, "/")
for key, values := range overlayURL.Query() {
query := merged.Query()
for _, v := range values {
query.Add(key, v)
}
merged.RawQuery = query.Encode()
}
}
return merged, nil
}
func (a *Authenticator) templatize(rawTemplate string, proxyName store.ProxyName, layerName store.LayerName) (string, error) {
tmpl, err := template.New("").Parse(rawTemplate)
if err != nil {
return "", errors.WithStack(err)
}
var raw bytes.Buffer
err = tmpl.Execute(&raw, struct {
ProxyName store.ProxyName
LayerName store.LayerName
}{
ProxyName: proxyName,
LayerName: layerName,
})
if err != nil {
return "", errors.WithStack(err)
}
return raw.String(), nil
} }
func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*Client, error) { func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*Client, error) {
ctx := context.Background() ctx := context.Background()
transport := a.httpTransport.Clone()
if options.OIDC.TLSInsecureSkipVerify {
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = true
}
httpClient := &http.Client{
Timeout: a.httpClientTimeout,
Transport: transport,
}
ctx = oidc.ClientContext(ctx, httpClient)
if options.OIDC.SkipIssuerVerification { if options.OIDC.SkipIssuerVerification {
ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL) ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL)
} }
@ -263,6 +395,7 @@ func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*C
WithRedirectURL(redirectURL), WithRedirectURL(redirectURL),
WithScopes(options.OIDC.Scopes...), WithScopes(options.OIDC.Scopes...),
WithAuthParams(options.OIDC.AuthParams), WithAuthParams(options.OIDC.AuthParams),
WithHTTPClient(httpClient),
) )
return client, nil return client, nil

View File

@ -6,7 +6,6 @@ import (
"net/url" "net/url"
"strings" "strings"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@ -30,6 +29,7 @@ var (
) )
type Client struct { type Client struct {
httpClient *http.Client
oauth2 *oauth2.Config oauth2 *oauth2.Config
provider *oidc.Provider provider *oidc.Provider
verifier *oidc.IDTokenVerifier verifier *oidc.IDTokenVerifier
@ -44,12 +44,12 @@ func (c *Client) Provider() *oidc.Provider {
return c.provider return c.provider
} }
func (c *Client) Authenticate(w http.ResponseWriter, r *http.Request, sess *sessions.Session) (*oidc.IDToken, error) { func (c *Client) Authenticate(w http.ResponseWriter, r *http.Request, sess *sessions.Session, postLoginRedirectURL string) (*oidc.IDToken, error) {
idToken, err := c.getIDToken(r, sess) idToken, err := c.getIDToken(r, sess)
if err != nil { if err != nil {
logger.Error(r.Context(), "could not retrieve idtoken", logger.E(errors.WithStack(err))) logger.Warn(r.Context(), "could not retrieve idtoken", logger.E(errors.WithStack(err)))
c.login(w, r, sess) c.login(w, r, sess, postLoginRedirectURL)
return nil, errors.WithStack(ErrLoginRequired) return nil, errors.WithStack(ErrLoginRequired)
} }
@ -57,23 +57,15 @@ func (c *Client) Authenticate(w http.ResponseWriter, r *http.Request, sess *sess
return idToken, nil return idToken, nil
} }
func (c *Client) login(w http.ResponseWriter, r *http.Request, sess *sessions.Session) { func (c *Client) login(w http.ResponseWriter, r *http.Request, sess *sessions.Session, postLoginRedirectURL string) {
ctx := r.Context() ctx := r.Context()
state := uniuri.New() state := uniuri.New()
nonce := uniuri.New() nonce := uniuri.New()
originalURL, err := director.OriginalURL(ctx)
if err != nil {
logger.Error(ctx, "could not retrieve original url", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
sess.Values[sessionKeyLoginState] = state sess.Values[sessionKeyLoginState] = state
sess.Values[sessionKeyLoginNonce] = nonce sess.Values[sessionKeyLoginNonce] = nonce
sess.Values[sessionKeyPostLoginRedirectURL] = originalURL.String() sess.Values[sessionKeyPostLoginRedirectURL] = postLoginRedirectURL
if err := sess.Save(r, w); err != nil { if err := sess.Save(r, w); err != nil {
logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
@ -210,6 +202,7 @@ func (c *Client) sessionEndURL(idTokenHint, state, postLogoutRedirectURL string)
func (c *Client) validate(r *http.Request, sess *sessions.Session) (*oauth2.Token, *oidc.IDToken, string, error) { func (c *Client) validate(r *http.Request, sess *sessions.Session) (*oauth2.Token, *oidc.IDToken, string, error) {
ctx := r.Context() ctx := r.Context()
ctx = oidc.ClientContext(ctx, c.httpClient)
rawStoredState := sess.Values[sessionKeyLoginState] rawStoredState := sess.Values[sessionKeyLoginState]
receivedState := r.URL.Query().Get("state") receivedState := r.URL.Query().Get("state")
@ -246,7 +239,7 @@ func (c *Client) validate(r *http.Request, sess *sessions.Session) (*oauth2.Toke
func (c *Client) getRawIDToken(sess *sessions.Session) (string, error) { func (c *Client) getRawIDToken(sess *sessions.Session) (string, error) {
rawIDToken, ok := sess.Values[sessionKeyIDToken].(string) rawIDToken, ok := sess.Values[sessionKeyIDToken].(string)
if !ok || rawIDToken == "" { if !ok || rawIDToken == "" {
return "", errors.New("invalid id token") return "", errors.New("id token not found")
} }
return rawIDToken, nil return rawIDToken, nil
@ -287,5 +280,6 @@ func NewClient(funcs ...ClientOptionFunc) *Client {
provider: opts.Provider, provider: opts.Provider,
verifier: verifier, verifier: verifier,
authParams: opts.AuthParams, authParams: opts.AuthParams,
httpClient: opts.HTTPClient,
} }
} }

View File

@ -2,6 +2,7 @@ package oidc
import ( import (
"context" "context"
"net/http"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
) )
@ -14,6 +15,7 @@ type ClientOptions struct {
Scopes []string Scopes []string
AuthParams map[string]string AuthParams map[string]string
SkipIssuerCheck bool SkipIssuerCheck bool
HTTPClient *http.Client
} }
type ClientOptionFunc func(*ClientOptions) type ClientOptionFunc func(*ClientOptions)
@ -63,9 +65,16 @@ func WithProvider(provider *oidc.Provider) ClientOptionFunc {
} }
} }
func WithHTTPClient(client *http.Client) ClientOptionFunc {
return func(opt *ClientOptions) {
opt.HTTPClient = client
}
}
func NewClientOptions(funcs ...ClientOptionFunc) *ClientOptions { func NewClientOptions(funcs ...ClientOptionFunc) *ClientOptions {
opt := &ClientOptions{ opt := &ClientOptions{
Scopes: []string{oidc.ScopeOpenID, "profile"}, Scopes: []string{oidc.ScopeOpenID, "profile"},
HTTPClient: http.DefaultClient,
} }
for _, f := range funcs { for _, f := range funcs {

View File

@ -17,9 +17,13 @@
"title": "URL de base du fournisseur OpenID Connect (racine du .well-known/openid-configuration)", "title": "URL de base du fournisseur OpenID Connect (racine du .well-known/openid-configuration)",
"type": "string" "type": "string"
}, },
"postLogoutRedirectURL": { "postLogoutRedirectURLs": {
"title": "URL de redirection après déconnexion", "title": "URLs de redirection après déconnexion autorisées",
"type": "string" "description": "La variable d'URL 'redirect=<url>' peut être utilisée pour spécifier une redirection après déconnexion.",
"type": "array",
"item": {
"type": "string"
}
}, },
"scopes": { "scopes": {
"title": "Scopes associés au client OpenID Connect", "title": "Scopes associés au client OpenID Connect",
@ -44,20 +48,43 @@
}, },
"loginCallbackPath": { "loginCallbackPath": {
"title": "Chemin associé à l'URL de callback OpenID Connect", "title": "Chemin associé à l'URL de callback OpenID Connect",
"default": "/.bouncer/authn/oidc/%s/callback", "default": "/.bouncer/authn/oidc/{{ .ProxyName }}/{{ .LayerName }}/callback",
"description": "Le marqueur '%s' peut être utilisé pour injecter l'espace de nom '<proxy>/<layer>'.", "description": "Les marqueurs '{{ .ProxyName }}' et '{{ .LayerName }}' peuvent être utilisés pour injecter le nom du proxy ainsi que celui du layer.",
"type": "string"
},
"matchLoginCallbackPath": {
"title": "Patron de correspondance du chemin interne de callback OpenID Connect",
"default": "*.bouncer/authn/oidc/{{ .ProxyName }}/{{ .LayerName }}/callback",
"description": "Les marqueurs '{{ .ProxyName }}' et '{{ .LayerName }}' peuvent être utilisés pour injecter le nom du proxy ainsi que celui du layer.",
"type": "string" "type": "string"
}, },
"logoutPath": { "logoutPath": {
"title": "Chemin associé à l'URL de déconnexion", "title": "Chemin associé à l'URL de déconnexion",
"default": "/.bouncer/authn/oidc/%s/logout", "default": "/.bouncer/authn/oidc/{{ .ProxyName }}/{{ .LayerName }}/logout",
"description": "Le marqueur '%s' peut être utilisé pour injecter l'espace de nom '<proxy>/<layer>'.", "description": "Les marqueurs '{{ .ProxyName }}' et '{{ .LayerName }}' peuvent être utilisés pour injecter le nom du proxy ainsi que celui du layer.",
"type": "string"
},
"publicBaseURL": {
"title": "URL publique de base associée au service distant",
"default": "",
"description": "Peut être utilisé par exemple si il y a discordance de nom d'hôte ou de chemin sur les URLs publiques/internes.",
"type": "string"
},
"matchLogoutPath": {
"title": "Patron de correspondance du chemin interne de déconnexion",
"default": "*.bouncer/authn/oidc/{{ .ProxyName }}/{{ .LayerName }}/logout",
"description": "Les marqueurs '{{ .ProxyName }}' et '{{ .LayerName }}' peuvent être utilisés pour injecter le nom du proxy ainsi que celui du layer.",
"type": "string" "type": "string"
}, },
"skipIssuerVerification": { "skipIssuerVerification": {
"title": "Activer/désactiver la vérification de concordance de l'identifiant du fournisseur d'identité", "title": "Activer/désactiver la vérification de concordance de l'identifiant du fournisseur d'identité",
"default": false, "default": false,
"type": "boolean" "type": "boolean"
},
"tlsInsecureSkipVerify": {
"title": "Activer/désactiver la vérification du certificat TLS distant",
"default": false,
"type": "boolean"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -8,6 +8,11 @@ import (
const LayerType store.LayerType = "authn-oidc" const LayerType store.LayerType = "authn-oidc"
func NewLayer(store sessions.Store) *authn.Layer { func NewLayer(store sessions.Store, funcs ...OptionFunc) *authn.Layer {
return authn.NewLayer(LayerType, &Authenticator{store: store}) opts := NewOptions(funcs...)
return authn.NewLayer(LayerType, &Authenticator{
httpTransport: opts.HTTPTransport,
httpClientTimeout: opts.HTTPClientTimeout,
store: store,
})
} }

View File

@ -19,11 +19,15 @@ type LayerOptions struct {
type OIDCOptions struct { type OIDCOptions struct {
ClientID string `mapstructure:"clientId"` ClientID string `mapstructure:"clientId"`
ClientSecret string `mapstructure:"clientSecret"` ClientSecret string `mapstructure:"clientSecret"`
PublicBaseURL string `mapstructure:"publicBaseURL"`
LoginCallbackPath string `mapstructure:"loginCallbackPath"` LoginCallbackPath string `mapstructure:"loginCallbackPath"`
MatchLoginCallbackPath string `mapstructure:"matchLoginCallbackPath"`
LogoutPath string `mapstructure:"logoutPath"` LogoutPath string `mapstructure:"logoutPath"`
MatchLogoutPath string `mapstructure:"matchLogoutPath"`
IssuerURL string `mapstructure:"issuerURL"` IssuerURL string `mapstructure:"issuerURL"`
SkipIssuerVerification bool `mapstructure:"skipIssuerVerification"` SkipIssuerVerification bool `mapstructure:"skipIssuerVerification"`
PostLogoutRedirectURL string `mapstructure:"postLogoutRedirectURL"` PostLogoutRedirectURLs []string `mapstructure:"postLogoutRedirectURLs"`
TLSInsecureSkipVerify bool `mapstructure:"tlsInsecureSkipVerify"`
Scopes []string `mapstructure:"scopes"` Scopes []string `mapstructure:"scopes"`
AuthParams map[string]string `mapstructure:"authParams"` AuthParams map[string]string `mapstructure:"authParams"`
} }
@ -39,12 +43,18 @@ type CookieOptions struct {
} }
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) { func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
loginCallbackPath := ".bouncer/authn/oidc/{{ .ProxyName }}/{{ .LayerName }}/callback"
logoutPath := ".bouncer/authn/oidc/{{ .ProxyName }}/{{ .LayerName }}/logout"
layerOptions := LayerOptions{ layerOptions := LayerOptions{
LayerOptions: authn.DefaultLayerOptions(), LayerOptions: authn.DefaultLayerOptions(),
OIDC: OIDCOptions{ OIDC: OIDCOptions{
LoginCallbackPath: "/.bouncer/authn/oidc/%s/callback", PublicBaseURL: "",
LogoutPath: "/.bouncer/authn/oidc/%s/logout", LoginCallbackPath: loginCallbackPath,
Scopes: []string{"openid"}, MatchLoginCallbackPath: "*" + loginCallbackPath,
LogoutPath: logoutPath,
MatchLogoutPath: "*" + logoutPath,
Scopes: []string{"openid"},
}, },
Cookie: CookieOptions{ Cookie: CookieOptions{
Name: defaultCookieName, Name: defaultCookieName,

View File

@ -0,0 +1,38 @@
package oidc
import (
"net/http"
"time"
)
type Options struct {
HTTPTransport *http.Transport
HTTPClientTimeout time.Duration
}
type OptionFunc func(opts *Options)
func WithHTTPTransport(transport *http.Transport) OptionFunc {
return func(opts *Options) {
opts.HTTPTransport = transport
}
}
func WithHTTPClientTimeout(timeout time.Duration) OptionFunc {
return func(opts *Options) {
opts.HTTPClientTimeout = timeout
}
}
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{
HTTPTransport: http.DefaultTransport.(*http.Transport),
HTTPClientTimeout: 30 * time.Second,
}
for _, fn := range funcs {
fn(opts)
}
return opts
}

View File

@ -0,0 +1,58 @@
package director
import (
"time"
"forge.cadoles.com/cadoles/bouncer/internal/cache"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
"forge.cadoles.com/cadoles/bouncer/internal/store"
)
type Options struct {
Layers []Layer
ProxyCache cache.Cache[string, []*store.Proxy]
LayerCache cache.Cache[string, []*store.Layer]
}
type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{
Layers: make([]Layer, 0),
ProxyCache: ttl.NewCache(
memory.NewCache[string, []*store.Proxy](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
LayerCache: ttl.NewCache(
memory.NewCache[string, []*store.Layer](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func WithLayers(layers ...Layer) OptionFunc {
return func(opts *Options) {
opts.Layers = layers
}
}
func WithProxyCache(cache cache.Cache[string, []*store.Proxy]) OptionFunc {
return func(opts *Options) {
opts.ProxyCache = cache
}
}
func WithLayerCache(cache cache.Cache[string, []*store.Layer]) OptionFunc {
return func(opts *Options) {
opts.LayerCache = cache
}
}

View File

@ -1,23 +1,27 @@
package proxy package proxy
import ( import (
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
) )
type Option struct { type Option struct {
ServerConfig config.ProxyServerConfig ServerConfig config.ProxyServerConfig
RedisConfig config.RedisConfig RedisConfig config.RedisConfig
DirectorLayers []director.Layer DirectorLayers []director.Layer
DirectorCacheTTL time.Duration
} }
type OptionFunc func(*Option) type OptionFunc func(*Option)
func defaultOption() *Option { func defaultOption() *Option {
return &Option{ return &Option{
ServerConfig: config.NewDefaultProxyServerConfig(), ServerConfig: config.NewDefaultProxyServerConfig(),
RedisConfig: config.NewDefaultRedisConfig(), RedisConfig: config.NewDefaultRedisConfig(),
DirectorLayers: make([]director.Layer, 0), DirectorLayers: make([]director.Layer, 0),
DirectorCacheTTL: 30 * time.Second,
} }
} }
@ -38,3 +42,9 @@ func WithDirectorLayers(layers ...director.Layer) OptionFunc {
opt.DirectorLayers = layers opt.DirectorLayers = layers
} }
} }
func WithDirectorCacheTTL(ttl time.Duration) OptionFunc {
return func(opt *Option) {
opt.DirectorCacheTTL = ttl
}
}

View File

@ -0,0 +1,156 @@
package proxy_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"testing"
"time"
"forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/store"
redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
func BenchmarkProxy(b *testing.B) {
redisEndpoint := os.Getenv("BOUNCER_BENCH_REDIS_ADDR")
if redisEndpoint == "" {
redisEndpoint = "127.0.0.1:6379"
}
client := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{redisEndpoint},
})
proxyRepository := redisStore.NewProxyRepository(client)
layerRepository := redisStore.NewLayerRepository(client)
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("Hello, world.")); err != nil {
b.Logf("[ERROR] %+v", errors.WithStack(err))
}
}))
defer backend.Close()
if err := waitFor(backend.URL, 5*time.Second); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
b.Logf("started backend '%s'", backend.URL)
ctx := context.Background()
proxyName := store.ProxyName(b.Name())
b.Logf("creating proxy '%s'", proxyName)
if err := proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
if _, err := proxyRepository.CreateProxy(ctx, proxyName, backend.URL, "*"); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil {
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
}
director := director.New(
proxyRepository, layerRepository,
director.WithLayerCache(
ttl.NewCache(
memory.NewCache[string, []*store.Layer](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
),
director.WithProxyCache(
ttl.NewCache(
memory.NewCache[string, []*store.Proxy](),
memory.NewCache[string, time.Time](),
30*time.Second,
),
),
)
directorMiddleware := director.Middleware()
handler := proxy.New(
proxy.WithRequestTransformers(
director.RequestTransformer(),
),
proxy.WithResponseTransformers(
director.ResponseTransformer(),
),
proxy.WithReverseProxyFactory(func(ctx context.Context, target *url.URL) *httputil.ReverseProxy {
reverse := httputil.NewSingleHostReverseProxy(target)
reverse.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
b.Logf("[ERROR] %s", errors.WithStack(err))
}
return reverse
}),
)
server := httptest.NewServer(directorMiddleware(handler))
defer server.Close()
b.Logf("started proxy '%s'", server.URL)
httpClient := server.Client()
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := httpClient.Get(server.URL)
if err != nil {
b.Errorf("could not fetch server url: %+v", errors.WithStack(err))
}
body, err := io.ReadAll(res.Body)
if err != nil {
b.Errorf("could not read response body: %+v", errors.WithStack(err))
}
b.Logf("%s - %v", res.Status, string(body))
if err := res.Body.Close(); err != nil {
b.Errorf("could not close response body: %+v", errors.WithStack(err))
}
}
}
func waitFor(url string, ttl time.Duration) error {
var lastErr error
timeout := time.After(ttl)
for {
select {
case <-timeout:
if lastErr != nil {
return lastErr
}
return errors.New("wait timed out")
default:
res, err := http.Get(url)
if err != nil {
lastErr = errors.WithStack(err)
continue
}
if res.StatusCode >= 200 && res.StatusCode < 400 {
return nil
}
}
}
}

View File

@ -11,6 +11,8 @@ import (
"time" "time"
"forge.cadoles.com/Cadoles/go-proxy" "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi" bouncerChi "forge.cadoles.com/cadoles/bouncer/internal/chi"
"forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
@ -25,11 +27,12 @@ import (
) )
type Server struct { type Server struct {
serverConfig config.ProxyServerConfig serverConfig config.ProxyServerConfig
redisConfig config.RedisConfig redisConfig config.RedisConfig
directorLayers []director.Layer directorLayers []director.Layer
proxyRepository store.ProxyRepository directorCacheTTL time.Duration
layerRepository store.LayerRepository proxyRepository store.ProxyRepository
layerRepository store.LayerRepository
} }
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) { func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
@ -86,7 +89,21 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
director := director.New( director := director.New(
s.proxyRepository, s.proxyRepository,
s.layerRepository, s.layerRepository,
s.directorLayers..., director.WithLayers(s.directorLayers...),
director.WithLayerCache(
ttl.NewCache(
memory.NewCache[string, []*store.Layer](),
memory.NewCache[string, time.Time](),
s.directorCacheTTL,
),
),
director.WithProxyCache(
ttl.NewCache(
memory.NewCache[string, []*store.Proxy](),
memory.NewCache[string, time.Time](),
s.directorCacheTTL,
),
),
) )
if s.serverConfig.HTTP.UseRealIP { if s.serverConfig.HTTP.UseRealIP {
@ -159,26 +176,10 @@ func (s *Server) createReverseProxy(ctx context.Context, target *url.URL) *httpu
DualStack: bool(dialConfig.DualStack), DualStack: bool(dialConfig.DualStack),
} }
transportConfig := s.serverConfig.Transport httpTransport := s.serverConfig.Transport.AsTransport()
httpTransport.DialContext = dialer.DialContext
reverseProxy.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: bool(transportConfig.ForceAttemptHTTP2),
MaxIdleConns: int(transportConfig.MaxIdleConns),
MaxIdleConnsPerHost: int(transportConfig.MaxIdleConnsPerHost),
MaxConnsPerHost: int(transportConfig.MaxConnsPerHost),
IdleConnTimeout: time.Duration(*transportConfig.IdleConnTimeout),
TLSHandshakeTimeout: time.Duration(*transportConfig.TLSHandshakeTimeout),
ExpectContinueTimeout: time.Duration(*transportConfig.ExpectContinueTimeout),
DisableKeepAlives: bool(transportConfig.DisableKeepAlives),
DisableCompression: bool(transportConfig.DisableCompression),
ResponseHeaderTimeout: time.Duration(*transportConfig.ResponseHeaderTimeout),
WriteBufferSize: int(transportConfig.WriteBufferSize),
ReadBufferSize: int(transportConfig.ReadBufferSize),
MaxResponseHeaderBytes: int64(transportConfig.MaxResponseHeaderBytes),
}
reverseProxy.Transport = httpTransport
reverseProxy.ErrorHandler = s.errorHandler reverseProxy.ErrorHandler = s.errorHandler
return reverseProxy return reverseProxy
@ -200,8 +201,9 @@ func NewServer(funcs ...OptionFunc) *Server {
} }
return &Server{ return &Server{
serverConfig: opt.ServerConfig, serverConfig: opt.ServerConfig,
redisConfig: opt.RedisConfig, redisConfig: opt.RedisConfig,
directorLayers: opt.DirectorLayers, directorLayers: opt.DirectorLayers,
directorCacheTTL: opt.DirectorCacheTTL,
} }
} }

View File

@ -1,6 +1,8 @@
package setup package setup
import ( import (
"time"
"forge.cadoles.com/cadoles/bouncer/internal/config" "forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
@ -25,5 +27,11 @@ func setupAuthnOIDCLayer(conf *config.Config) (director.Layer, error) {
adapter := redis.NewStoreAdapter(rdb) adapter := redis.NewStoreAdapter(rdb)
store := session.NewStore(adapter) store := session.NewStore(adapter)
return oidc.NewLayer(store), nil transport := conf.Layers.Authn.OIDC.HTTPClient.AsTransport()
return oidc.NewLayer(
store,
oidc.WithHTTPTransport(transport),
oidc.WithHTTPClientTimeout(time.Duration(*conf.Layers.Authn.OIDC.HTTPClient.Timeout)),
), nil
} }

View File

@ -93,6 +93,14 @@ proxy:
credentials: credentials:
prom: etheus prom: etheus
# Configuration de la mise en cache
# locale des données proxy/layers
cache:
# Les proxys/layers sont mis en cache local pour une durée de 30s
# par défaut. Si les modifications sont rares, vous pouvez augmenter
# cette valeur pour réduire la "pression" sur le serveur Redis.
ttl: 30s
# Configuration du transport HTTP(S) # Configuration du transport HTTP(S)
# Voir https://pkg.go.dev/net/http#Transport # Voir https://pkg.go.dev/net/http#Transport
transport: transport: