diff --git a/.golangci.yml b/.golangci.yml index edab12d0..62e1df8e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -37,7 +37,7 @@ linters: - linters: - revive path: _test\.go - text: 'dot-imports:' + text: "dot-imports:" # # If we have tests in shared test folders, these can be less strictly linted - linters: - bodyclose diff --git a/Makefile b/Makefile index 091ca726..1299a529 100644 --- a/Makefile +++ b/Makefile @@ -75,6 +75,10 @@ DOCKER_BUILDX_PUSH_X_PLATFORM_ALPINE := $(DOCKER_BUILDX_X_PLATFORM_ALPINE) --pus .PHONY: build-docker build-docker: build-distroless build-alpine ## Build multi architecture docker images in both flavours (distroless / alpine) +.PHONY: build-docker-local +build-docker-local: ## Build distroless docker image and locally load into docker images + $(DOCKER_BUILDX) --load -t $(REGISTRY)/$(REPOSITORY):${VERSION}-local . + .PHONY: build-distroless build-distroless: ## Build multi architecture distroless based docker image $(DOCKER_BUILDX_X_PLATFORM) -t $(REGISTRY)/$(REPOSITORY):latest -t $(REGISTRY)/$(REPOSITORY):${VERSION} . diff --git a/contrib/local-environment/docker-compose-alpha-config.yaml b/contrib/local-environment/docker-compose-alpha-config.yaml index a43dc457..42bb2d1f 100644 --- a/contrib/local-environment/docker-compose-alpha-config.yaml +++ b/contrib/local-environment/docker-compose-alpha-config.yaml @@ -10,11 +10,58 @@ # make alpha-config- (eg make nginx-up, make nginx-down) # # Access http://localhost:4180 to initiate a login cycle -version: '3.0' +version: "3.0" services: oauth2-proxy: + container_name: oauth2-proxy image: quay.io/oauth2-proxy/oauth2-proxy:v7.12.0 command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml + hostname: oauth2-proxy volumes: - "./oauth2-proxy-alpha-config.cfg:/oauth2-proxy.cfg" - "./oauth2-proxy-alpha-config.yaml:/oauth2-proxy-alpha-config.yaml" + restart: unless-stopped + ports: + - 4180:4180/tcp + networks: + dex: {} + httpbin: {} + depends_on: + - dex + - httpbin + dex: + container_name: dex + image: ghcr.io/dexidp/dex:v2.43.1 + command: dex serve /dex.yaml + hostname: dex + volumes: + - "./dex.yaml:/dex.yaml" + restart: unless-stopped + ports: + - 5556:5556/tcp + networks: + dex: + aliases: + - dex.localtest.me + etcd: {} + depends_on: + - etcd + httpbin: + container_name: httpbin + image: kennethreitz/httpbin + ports: [] + networks: + httpbin: {} + etcd: + container_name: etcd + image: gcr.io/etcd-development/etcd:v3.6.2 + entrypoint: /usr/local/bin/etcd + command: + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://etcd:2379 + networks: + etcd: {} +networks: + dex: {} + etcd: {} + httpbin: {} diff --git a/contrib/local-environment/docker-compose.yaml b/contrib/local-environment/docker-compose.yaml index 6490ca8e..b787e9e0 100644 --- a/contrib/local-environment/docker-compose.yaml +++ b/contrib/local-environment/docker-compose.yaml @@ -9,7 +9,7 @@ # make (eg. make up, make down) # # Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle -version: '3.0' +version: "3.0" services: oauth2-proxy: container_name: oauth2-proxy diff --git a/contrib/local-environment/oauth2-proxy-alpha-config.cfg b/contrib/local-environment/oauth2-proxy-alpha-config.cfg index 89e5a5b2..c913ec4f 100644 --- a/contrib/local-environment/oauth2-proxy-alpha-config.cfg +++ b/contrib/local-environment/oauth2-proxy-alpha-config.cfg @@ -1,5 +1,4 @@ -http_address="0.0.0.0:4180" cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" email_domains="example.com" cookie_secure="false" -redirect_url="http://localhost:4180/oauth2/callback" +redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback" diff --git a/contrib/local-environment/oauth2-proxy-alpha-config.yaml b/contrib/local-environment/oauth2-proxy-alpha-config.yaml index b2c9f6a8..e423db98 100644 --- a/contrib/local-environment/oauth2-proxy-alpha-config.yaml +++ b/contrib/local-environment/oauth2-proxy-alpha-config.yaml @@ -1,23 +1,23 @@ -upstreams: - - id: httpbin - path: / - uri: http://httpbin +server: + bindAddress: "0.0.0.0:4180" +upstreamConfig: + upstreams: + - id: httpbin + path: / + uri: http://httpbin injectRequestHeaders: -- name: X-Forwarded-Groups - values: - - claim: groups -- name: X-Forwarded-User - values: - - claim: user -- name: X-Forwarded-Email - values: - - claim: email -- name: X-Forwarded-Preferred-Username - values: - - claim: preferred_username + - name: X-Forwarded-User + values: + - claimSource: + claim: user + - name: X-Forwarded-Email + values: + - claimSource: + claim: email providers: -- provider: oidc - clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK - clientID: oauth2-proxy - oidcConfig: - issuerURL: http://dex.localhost:5556/dex + - id: oidc + provider: oidc + clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK + clientID: oauth2-proxy + oidcConfig: + issuerURL: http://dex.localtest.me:5556/dex diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index 28645ceb..2be241a7 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -204,16 +204,6 @@ ClaimSource allows loading a header value from a claim within the session | `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the
claim if it is non-empty. | | `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.
Note the value of claim will become the basic auth username and the
basicAuthPassword will be used as the password value. | -### Duration -#### (`string` alias) - -(**Appears on:** [Upstream](#upstream)) - -Duration is as string representation of a period of time. -A duration string is a is a possibly signed sequence of decimal numbers, -each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". -Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". - ### GitHubOptions (**Appears on:** [Provider](#provider)) @@ -499,9 +489,9 @@ Server represents the configuration for an HTTP(S) server | Field | Type | Description | | ----- | ---- | ----------- | -| `BindAddress` | _string_ | BindAddress is the address on which to serve traffic.
Leave blank or set to "-" to disable. | -| `SecureBindAddress` | _string_ | SecureBindAddress is the address on which to serve secure traffic.
Leave blank or set to "-" to disable. | -| `TLS` | _[TLS](#tls)_ | TLS contains the information for loading the certificate and key for the
secure traffic and further configuration for the TLS server. | +| `bindAddress` | _string_ | BindAddress is the address on which to serve traffic.
Leave blank or set to "-" to disable. | +| `secureBindAddress` | _string_ | SecureBindAddress is the address on which to serve secure traffic.
Leave blank or set to "-" to disable. | +| `tls` | _[TLS](#tls)_ | TLS contains the information for loading the certificate and key for the
secure traffic and further configuration for the TLS server. | ### TLS @@ -512,10 +502,10 @@ as well as an optional minimal TLS version that is acceptable. | Field | Type | Description | | ----- | ---- | ----------- | -| `Key` | _[SecretSource](#secretsource)_ | Key is the TLS key data to use.
Typically this will come from a file. | -| `Cert` | _[SecretSource](#secretsource)_ | Cert is the TLS certificate data to use.
Typically this will come from a file. | -| `MinVersion` | _string_ | MinVersion is the minimal TLS version that is acceptable.
E.g. Set to "TLS1.3" to select TLS version 1.3 | -| `CipherSuites` | _[]string_ | CipherSuites is a list of TLS cipher suites that are allowed.
E.g.:
- TLS_RSA_WITH_RC4_128_SHA
- TLS_RSA_WITH_AES_256_GCM_SHA384
If not specified, the default Go safe cipher list is used.
List of valid cipher suites can be found in the [crypto/tls documentation](https://pkg.go.dev/crypto/tls#pkg-constants). | +| `key` | _[SecretSource](#secretsource)_ | Key is the TLS key data to use.
Typically this will come from a file. | +| `cert` | _[SecretSource](#secretsource)_ | Cert is the TLS certificate data to use.
Typically this will come from a file. | +| `minVersion` | _string_ | MinVersion is the minimal TLS version that is acceptable.
E.g. Set to "TLS1.3" to select TLS version 1.3 | +| `cipherSuites` | _[]string_ | CipherSuites is a list of TLS cipher suites that are allowed.
E.g.:
- TLS_RSA_WITH_RC4_128_SHA
- TLS_RSA_WITH_AES_256_GCM_SHA384
If not specified, the default Go safe cipher list is used.
List of valid cipher suites can be found in the [crypto/tls documentation](https://pkg.go.dev/crypto/tls#pkg-constants). | ### URLParameterRule @@ -547,10 +537,10 @@ Requests will be proxied to this upstream if the path matches the request path. | `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.
This option is insecure and will allow potential Man-In-The-Middle attacks
between OAuth2 Proxy and the upstream server.
Defaults to false. | | `static` | _bool_ | Static will make all requests to this upstream have a static response.
The response will have a body of "Authenticated" and a response code
matching StaticCode.
If StaticCode is not set, the response will return a 200 response. | | `staticCode` | _int_ | StaticCode determines the response code for the Static response.
This option can only be used with Static enabled. | -| `flushInterval` | _[Duration](#duration)_ | FlushInterval is the period between flushing the response buffer when
streaming response from the upstream.
Defaults to 1 second. | +| `flushInterval` | _duration_ | FlushInterval is the period between flushing the response buffer when
streaming response from the upstream.
Defaults to 1 second. | | `passHostHeader` | _bool_ | PassHostHeader determines whether the request host header should be proxied
to the upstream server.
Defaults to true. | | `proxyWebSockets` | _bool_ | ProxyWebSockets enables proxying of websockets to upstream servers
Defaults to true. | -| `timeout` | _[Duration](#duration)_ | Timeout is the maximum duration the server will wait for a response from the upstream server.
Defaults to 30 seconds. | +| `timeout` | _duration_ | Timeout is the maximum duration the server will wait for a response from the upstream server.
Defaults to 30 seconds. | | `disableKeepAlives` | _bool_ | DisableKeepAlives disables HTTP keep-alive connections to the upstream server.
Defaults to false. | ### UpstreamConfig diff --git a/go.mod b/go.mod index 24f316e4..2fbfed82 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/fsnotify/fsnotify v1.9.0 - github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/go-jose/go-jose/v3 v3.0.4 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.2.3 @@ -24,7 +23,7 @@ require ( github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.37.0 + github.com/onsi/gomega v1.38.0 github.com/pierrec/lz4/v4 v4.1.22 github.com/prometheus/client_golang v1.22.0 github.com/redis/go-redis/v9 v9.11.0 @@ -37,13 +36,14 @@ require ( golang.org/x/net v0.42.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.16.0 - google.golang.org/api v0.242.0 + google.golang.org/api v0.243.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.33.3 ) require ( - cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect @@ -83,9 +83,8 @@ require ( golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.35.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/grpc v1.73.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect + google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index caa8e2a0..8a0826d6 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,11 @@ -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= -github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= @@ -20,12 +19,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw= github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -44,12 +38,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= -github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= @@ -61,17 +51,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY= github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -85,23 +68,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvBn0G0a7QBRdBUp6Y048BtLAHRTKPo= github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s= @@ -111,8 +85,8 @@ github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3Tc github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= +github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= @@ -120,8 +94,6 @@ github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -132,7 +104,6 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= @@ -142,8 +113,6 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= @@ -165,18 +134,12 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -186,8 +149,6 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -197,8 +158,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -206,8 +165,6 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -219,8 +176,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -234,53 +189,29 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= -google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE= -google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= -google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= -google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= -google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= -google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/api v0.243.0 h1:sw+ESIJ4BVnlJcWu9S+p2Z6Qq1PjG77T8IJ1xtp4jZQ= +google.golang.org/api v0.243.0/go.mod h1:GE4QtYfaybx1KmeHMdBnNnyLzBZCVihGBXAmJu/uUr8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= diff --git a/main.go b/main.go index cf7e964c..1525e94c 100644 --- a/main.go +++ b/main.go @@ -5,12 +5,12 @@ import ( "os" "runtime" - "github.com/ghodss/yaml" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/version" "github.com/spf13/pflag" + "gopkg.in/yaml.v3" ) func main() { @@ -67,12 +67,18 @@ func main() { // loadConfiguration will load in the user's configuration. // It will either load the alpha configuration (if alphaConfig is given) // or the legacy configuration. -func loadConfiguration(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { - if alphaConfig != "" { - logger.Printf("WARNING: You are using alpha configuration. The structure in this configuration file may change without notice. You MUST remove conflicting options from your existing configuration.") - return loadAlphaOptions(config, alphaConfig, extraFlags, args) +func loadConfiguration(config, yamlConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { + opts, err := loadLegacyOptions(config, extraFlags, args) + if err != nil { + return nil, fmt.Errorf("failed to load legacy options: %w", err) } - return loadLegacyOptions(config, extraFlags, args) + + if yamlConfig != "" { + logger.Printf("WARNING: You are using alpha configuration. The structure in this configuration file may change without notice. You MUST remove conflicting options from your existing configuration.") + return loadYamlOptions(yamlConfig, config, extraFlags, args) + } + + return opts, nil } // loadLegacyOptions loads the old toml options using the legacy flagset @@ -97,17 +103,17 @@ func loadLegacyOptions(config string, extraFlags *pflag.FlagSet, args []string) return opts, nil } -// loadAlphaOptions loads the old style config excluding options converted to +// loadYamlOptions loads the old style config excluding options converted to // the new alpha format, then merges the alpha options, loaded from YAML, // into the core configuration. -func loadAlphaOptions(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { +func loadYamlOptions(yamlConfig, config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { opts, err := loadOptions(config, extraFlags, args) if err != nil { return nil, fmt.Errorf("failed to load core options: %v", err) } - alphaOpts := &options.AlphaOptions{} - if err := options.LoadYAML(alphaConfig, alphaOpts); err != nil { + alphaOpts := options.NewAlphaOptions(opts) + if err := options.LoadYAML(yamlConfig, alphaOpts); err != nil { return nil, fmt.Errorf("failed to load alpha options: %v", err) } @@ -137,10 +143,16 @@ func loadOptions(config string, extraFlags *pflag.FlagSet, args []string) (*opti // printConvertedConfig extracts alpha options from the loaded configuration // and renders these to stdout in YAML format. func printConvertedConfig(opts *options.Options) error { - alphaConfig := &options.AlphaOptions{} - alphaConfig.ExtractFrom(opts) + alphaConfig := options.NewAlphaOptions(opts) - data, err := yaml.Marshal(alphaConfig) + // Generic interface for loading arbitrary yaml structure + var buffer map[string]interface{} + + if err := options.Decode(alphaConfig, &buffer); err != nil { + return fmt.Errorf("unable to decode alpha config into interface: %w", err) + } + + data, err := yaml.Marshal(buffer) if err != nil { return fmt.Errorf("unable to marshal config: %v", err) } diff --git a/main_test.go b/main_test.go index 8e40fe7f..b9ec39fb 100644 --- a/main_test.go +++ b/main_test.go @@ -2,13 +2,12 @@ package main import ( "errors" - "fmt" "os" - "strings" "time" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" . "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/testutil" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/format" @@ -24,14 +23,15 @@ var _ = Describe("Configuration Loading Suite", func() { http_address="127.0.0.1:4180" upstreams="http://httpbin" set_basic_auth="true" -basic_auth_password="super-secret-password" +basic_auth_password="c3VwZXItc2VjcmV0LXBhc3N3b3Jk" client_id="oauth2-proxy" client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" +google_admin_email="admin@example.com" +google_target_principal="principal" ` const testAlphaConfig = ` upstreamConfig: - proxyrawpath: false upstreams: - id: / path: / @@ -40,45 +40,63 @@ upstreamConfig: passHostHeader: true proxyWebSockets: true timeout: 30s + insecureSkipTLSVerify: false + disableKeepAlives: false injectRequestHeaders: - name: Authorization + preserveRequestValue: false values: - - claim: user - prefix: "Basic " - basicAuthPassword: - value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk + - claimSource: + claim: user + prefix: "Basic " + basicAuthPassword: + value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk - name: X-Forwarded-Groups + preserveRequestValue: false values: - - claim: groups + - claimSource: + claim: groups - name: X-Forwarded-User + preserveRequestValue: false values: - - claim: user + - claimSource: + claim: user - name: X-Forwarded-Email + preserveRequestValue: false values: - - claim: email + - claimSource: + claim: email - name: X-Forwarded-Preferred-Username + preserveRequestValue: false values: - - claim: preferred_username + - claimSource: + claim: preferred_username injectResponseHeaders: - name: Authorization values: - - claim: user - prefix: "Basic " - basicAuthPassword: - value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk + - claimSource: + claim: user + prefix: "Basic " + basicAuthPassword: + value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk server: bindAddress: "127.0.0.1:4180" providers: -- provider: google - ID: google=oauth2-proxy +- id: google=oauth2-proxy + provider: google clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK clientID: oauth2-proxy - azureConfig: - tenant: common + useSystemTrustStore: false + skipClaimsFromProfileURL: false + googleConfig: + adminEmail: admin@example.com + targetPrincipal: principal + useApplicationDefaultCredentials: false oidcConfig: groupsClaim: groups emailClaim: email userIDClaim: email + insecureSkipIssuerVerification: false insecureSkipNonce: true audienceClaims: [aud] extraAudiences: [] @@ -96,13 +114,8 @@ cookie_secure="false" redirect_url="http://localhost:4180/oauth2/callback" ` - boolPtr := func(b bool) *bool { - return &b - } - - durationPtr := func(d time.Duration) *options.Duration { - du := options.Duration(d) - return &du + durationPtr := func(d time.Duration) *time.Duration { + return &d } testExpectedOptions := func() *options.Options { @@ -117,13 +130,15 @@ redirect_url="http://localhost:4180/oauth2/callback" opts.UpstreamServers = options.UpstreamConfig{ Upstreams: []options.Upstream{ { - ID: "/", - Path: "/", - URI: "http://httpbin", - FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval), - PassHostHeader: boolPtr(true), - ProxyWebSockets: boolPtr(true), - Timeout: durationPtr(options.DefaultUpstreamTimeout), + ID: "/", + Path: "/", + URI: "http://httpbin", + FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval), + PassHostHeader: ptr.Ptr(true), + ProxyWebSockets: ptr.Ptr(true), + Timeout: durationPtr(options.DefaultUpstreamTimeout), + InsecureSkipTLSVerify: ptr.Ptr(false), + DisableKeepAlives: ptr.Ptr(false), }, }, } @@ -136,32 +151,45 @@ redirect_url="http://localhost:4180/oauth2/callback" Claim: "user", Prefix: "Basic ", BasicAuthPassword: &options.SecretSource{ - Value: []byte("super-secret-password"), + Value: []byte("c3VwZXItc2VjcmV0LXBhc3N3b3Jk"), }, }, }, }, } + authHeader.PreserveRequestValue = ptr.Ptr(false) opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...) + + authHeader.PreserveRequestValue = nil opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader) opts.Providers = options.Providers{ options.Provider{ - ID: "google=oauth2-proxy", - Type: "google", - ClientSecret: "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK", - ClientID: "oauth2-proxy", + ID: "google=oauth2-proxy", + Type: "google", + ClientSecret: "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK", + ClientID: "oauth2-proxy", + UseSystemTrustStore: ptr.Ptr(false), + SkipClaimsFromProfileURL: ptr.Ptr(false), + GoogleConfig: options.GoogleOptions{ + AdminEmail: "admin@example.com", + UseApplicationDefaultCredentials: ptr.Ptr(false), + TargetPrincipal: "principal", + }, AzureConfig: options.AzureOptions{ Tenant: "common", }, OIDCConfig: options.OIDCOptions{ - GroupsClaim: "groups", - EmailClaim: "email", - UserIDClaim: "email", - AudienceClaims: []string{"aud"}, - ExtraAudiences: []string{}, - InsecureSkipNonce: true, + GroupsClaim: "groups", + EmailClaim: "email", + UserIDClaim: "email", + AudienceClaims: []string{"aud"}, + ExtraAudiences: []string{}, + InsecureSkipNonce: ptr.Ptr(true), + InsecureAllowUnverifiedEmail: ptr.Ptr(false), + InsecureSkipIssuerVerification: ptr.Ptr(false), + SkipDiscovery: ptr.Ptr(false), }, LoginURLParameters: []options.LoginURLParameter{ {Name: "approval_prompt", Default: []string{"force"}}, @@ -226,7 +254,7 @@ redirect_url="http://localhost:4180/oauth2/callback" opts, err := loadConfiguration(configFileName, alphaConfigFileName, extraFlags, in.args) if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) + Expect(err).To(MatchError(ContainSubstring(in.expectedErr.Error()))) } else { Expect(err).ToNot(HaveOccurred()) } @@ -245,19 +273,19 @@ redirect_url="http://localhost:4180/oauth2/callback" Entry("with bad legacy configuration", loadConfigurationTableInput{ configContent: testCoreConfig + "unknown_field=\"something\"", expectedOptions: func() *options.Options { return nil }, - expectedErr: errors.New("failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"), + expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"), }), Entry("with bad alpha configuration", loadConfigurationTableInput{ configContent: testCoreConfig, alphaConfigContent: testAlphaConfig + ":", expectedOptions: func() *options.Options { return nil }, - expectedErr: fmt.Errorf("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line %d: did not find expected key", strings.Count(testAlphaConfig, "\n")), + expectedErr: errors.New("failed to load alpha options: error unmarshalling config: yaml: line 1: did not find expected key"), }), Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{ configContent: testCoreConfig + "unknown_field=\"something\"", alphaConfigContent: testAlphaConfig, expectedOptions: func() *options.Options { return nil }, - expectedErr: errors.New("failed to load core options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"), + expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"), }), ) }) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 488b8cea..a156214c 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -23,6 +23,7 @@ import ( internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc" sessionscookie "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" "github.com/oauth2-proxy/oauth2-proxy/v7/providers" "github.com/stretchr/testify/assert" @@ -506,7 +507,7 @@ func TestStaticProxyUpstream(t *testing.T) { ProxyUpstream: options.Upstream{ ID: "static-proxy", Path: "/static-proxy", - Static: true, + Static: ptr.Ptr(true), }, }) if err != nil { @@ -2223,7 +2224,7 @@ func TestTrustedIPs(t *testing.T) { { ID: "static", Path: "/", - Static: true, + Static: ptr.Ptr(true), }, }, } diff --git a/pkg/apis/options/alpha_options.go b/pkg/apis/options/alpha_options.go index a438518c..0c78359a 100644 --- a/pkg/apis/options/alpha_options.go +++ b/pkg/apis/options/alpha_options.go @@ -12,13 +12,13 @@ type AlphaOptions struct { // UpstreamConfig is used to configure upstream servers. // Once a user is authenticated, requests to the server will be proxied to // these upstream servers based on the path mappings defined in this list. - UpstreamConfig UpstreamConfig `json:"upstreamConfig,omitempty"` + UpstreamConfig UpstreamConfig `yaml:"upstreamConfig,omitempty"` // InjectRequestHeaders is used to configure headers that should be added // to requests to upstream servers. // Headers may source values from either the authenticated user's session // or from a static secret value. - InjectRequestHeaders []Header `json:"injectRequestHeaders,omitempty"` + InjectRequestHeaders []Header `yaml:"injectRequestHeaders,omitempty"` // InjectResponseHeaders is used to configure headers that should be added // to responses from the proxy. @@ -27,35 +27,31 @@ type AlphaOptions struct { // auth_request module. // Headers may source values from either the authenticated user's session // or from a static secret value. - InjectResponseHeaders []Header `json:"injectResponseHeaders,omitempty"` + InjectResponseHeaders []Header `yaml:"injectResponseHeaders,omitempty"` // Server is used to configure the HTTP(S) server for the proxy application. // You may choose to run both HTTP and HTTPS servers simultaneously. // This can be done by setting the BindAddress and the SecureBindAddress simultaneously. // To use the secure server you must configure a TLS certificate and key. - Server Server `json:"server,omitempty"` + Server Server `yaml:"server,omitempty"` // MetricsServer is used to configure the HTTP(S) server for metrics. // You may choose to run both HTTP and HTTPS servers simultaneously. // This can be done by setting the BindAddress and the SecureBindAddress simultaneously. // To use the secure server you must configure a TLS certificate and key. - MetricsServer Server `json:"metricsServer,omitempty"` + MetricsServer Server `yaml:"metricsServer,omitempty"` // Providers is used to configure your provider. **Multiple-providers is not // yet working.** [This feature is tracked in // #925](https://github.com/oauth2-proxy/oauth2-proxy/issues/926) - Providers Providers `json:"providers,omitempty"` + Providers Providers `yaml:"providers,omitempty"` } -// MergeInto replaces alpha options in the Options struct with the values -// from the AlphaOptions -func (a *AlphaOptions) MergeInto(opts *Options) { - opts.UpstreamServers = a.UpstreamConfig - opts.InjectRequestHeaders = a.InjectRequestHeaders - opts.InjectResponseHeaders = a.InjectResponseHeaders - opts.Server = a.Server - opts.MetricsServer = a.MetricsServer - opts.Providers = a.Providers +// Initialize alpha options with default values and settings of the core options +func NewAlphaOptions(opts *Options) *AlphaOptions { + aOpts := &AlphaOptions{} + aOpts.ExtractFrom(opts) + return aOpts } // ExtractFrom populates the fields in the AlphaOptions with the values from @@ -68,3 +64,14 @@ func (a *AlphaOptions) ExtractFrom(opts *Options) { a.MetricsServer = opts.MetricsServer a.Providers = opts.Providers } + +// MergeInto replaces alpha options in the Options struct with the values +// from the AlphaOptions +func (a *AlphaOptions) MergeInto(opts *Options) { + opts.UpstreamServers = a.UpstreamConfig + opts.InjectRequestHeaders = a.InjectRequestHeaders + opts.InjectResponseHeaders = a.InjectResponseHeaders + opts.Server = a.Server + opts.MetricsServer = a.MetricsServer + opts.Providers = a.Providers +} diff --git a/pkg/apis/options/common.go b/pkg/apis/options/common.go deleted file mode 100644 index 88d24d82..00000000 --- a/pkg/apis/options/common.go +++ /dev/null @@ -1,63 +0,0 @@ -package options - -import ( - "fmt" - "strconv" - "time" -) - -// SecretSource references an individual secret value. -// Only one source within the struct should be defined at any time. -type SecretSource struct { - // Value expects a base64 encoded string value. - Value []byte `json:"value,omitempty"` - - // FromEnv expects the name of an environment variable. - FromEnv string `json:"fromEnv,omitempty"` - - // FromFile expects a path to a file containing the secret value. - FromFile string `json:"fromFile,omitempty"` -} - -// Duration is an alias for time.Duration so that we can ensure the marshalling -// and unmarshalling of string durations is done as users expect. -// Intentional blank line below to keep this first part of the comment out of -// any generated references. - -// Duration is as string representation of a period of time. -// A duration string is a is a possibly signed sequence of decimal numbers, -// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". -// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -// +reference-gen:alias-name=string -type Duration time.Duration - -// UnmarshalJSON parses the duration string and sets the value of duration -// to the value of the duration string. -func (d *Duration) UnmarshalJSON(data []byte) error { - input := string(data) - if unquoted, err := strconv.Unquote(input); err == nil { - input = unquoted - } - - du, err := time.ParseDuration(input) - if err != nil { - return err - } - *d = Duration(du) - return nil -} - -// MarshalJSON ensures that when the string is marshalled to JSON as a human -// readable string. -func (d *Duration) MarshalJSON() ([]byte, error) { - dStr := fmt.Sprintf("%q", d.Duration().String()) - return []byte(dStr), nil -} - -// Duration returns the time.Duration version of this Duration -func (d *Duration) Duration() time.Duration { - if d == nil { - return time.Duration(0) - } - return time.Duration(*d) -} diff --git a/pkg/apis/options/common_test.go b/pkg/apis/options/common_test.go deleted file mode 100644 index db33a58b..00000000 --- a/pkg/apis/options/common_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package options - -import ( - "encoding/json" - "errors" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Common", func() { - Context("Duration", func() { - type marshalJSONTableInput struct { - duration Duration - expectedJSON string - } - - DescribeTable("MarshalJSON", - func(in marshalJSONTableInput) { - data, err := in.duration.MarshalJSON() - Expect(err).ToNot(HaveOccurred()) - Expect(string(data)).To(Equal(in.expectedJSON)) - - var d Duration - Expect(json.Unmarshal(data, &d)).To(Succeed()) - Expect(d).To(Equal(in.duration)) - }, - Entry("30 seconds", marshalJSONTableInput{ - duration: Duration(30 * time.Second), - expectedJSON: "\"30s\"", - }), - Entry("1 minute", marshalJSONTableInput{ - duration: Duration(1 * time.Minute), - expectedJSON: "\"1m0s\"", - }), - Entry("1 hour 15 minutes", marshalJSONTableInput{ - duration: Duration(75 * time.Minute), - expectedJSON: "\"1h15m0s\"", - }), - Entry("A zero Duration", marshalJSONTableInput{ - duration: Duration(0), - expectedJSON: "\"0s\"", - }), - ) - - type unmarshalJSONTableInput struct { - json string - expectedErr error - expectedDuration Duration - } - - DescribeTable("UnmarshalJSON", - func(in unmarshalJSONTableInput) { - // A duration must be initialised pointer before UnmarshalJSON will work. - zero := Duration(0) - d := &zero - - err := d.UnmarshalJSON([]byte(in.json)) - if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) - } else { - Expect(err).ToNot(HaveOccurred()) - } - Expect(d).ToNot(BeNil()) - Expect(*d).To(Equal(in.expectedDuration)) - }, - Entry("1m", unmarshalJSONTableInput{ - json: "\"1m\"", - expectedDuration: Duration(1 * time.Minute), - }), - Entry("30s", unmarshalJSONTableInput{ - json: "\"30s\"", - expectedDuration: Duration(30 * time.Second), - }), - Entry("1h15m", unmarshalJSONTableInput{ - json: "\"1h15m\"", - expectedDuration: Duration(75 * time.Minute), - }), - Entry("am", unmarshalJSONTableInput{ - json: "\"am\"", - expectedErr: errors.New("time: invalid duration \"am\""), - expectedDuration: Duration(0), - }), - ) - }) -}) diff --git a/pkg/apis/options/doc.go b/pkg/apis/options/doc.go index 8ef112dd..d4b8862d 100644 --- a/pkg/apis/options/doc.go +++ b/pkg/apis/options/doc.go @@ -1,3 +1,3 @@ -//go:generate -command reference-gen go run github.com/oauth2-proxy/tools/reference-gen/cmd/reference-gen@v0.0.0-20220223111546-d3b50d1a591a +//go:generate -command reference-gen go run github.com/oauth2-proxy/tools/reference-gen/cmd/reference-gen@v0.0.0-20250404153144-32055bc45bc3 //go:generate reference-gen --package github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options --types AlphaOptions --header-file ../../../docs/docs/configuration/alpha_config.md.tmpl --out-file ../../../docs/docs/configuration/alpha_config.md package options diff --git a/pkg/apis/options/header.go b/pkg/apis/options/header.go index 90e6445c..b50b9582 100644 --- a/pkg/apis/options/header.go +++ b/pkg/apis/options/header.go @@ -5,26 +5,26 @@ package options type Header struct { // Name is the header name to be used for this set of values. // Names should be unique within a list of Headers. - Name string `json:"name,omitempty"` + Name string `yaml:"name,omitempty"` // PreserveRequestValue determines whether any values for this header // should be preserved for the request to the upstream server. // This option only applies to injected request headers. // Defaults to false (headers that match this header will be stripped). - PreserveRequestValue bool `json:"preserveRequestValue,omitempty"` + PreserveRequestValue *bool `yaml:"preserveRequestValue,omitempty"` // Values contains the desired values for this header - Values []HeaderValue `json:"values,omitempty"` + Values []HeaderValue `yaml:"values,omitempty"` } // HeaderValue represents a single header value and the sources that can // make up the header value type HeaderValue struct { // Allow users to load the value from a secret source - *SecretSource `json:",omitempty"` + *SecretSource `yaml:"secretSource,omitempty"` // Allow users to load the value from a session claim - *ClaimSource `json:",omitempty"` + *ClaimSource `yaml:"claimSource,omitempty"` } // ClaimSource allows loading a header value from a claim within the session @@ -32,14 +32,14 @@ type ClaimSource struct { // Claim is the name of the claim in the session that the value should be // loaded from. Available claims: `access_token` `id_token` `created_at` // `expires_on` `refresh_token` `email` `user` `groups` `preferred_username`. - Claim string `json:"claim,omitempty"` + Claim string `yaml:"claim,omitempty"` // Prefix is an optional prefix that will be prepended to the value of the // claim if it is non-empty. - Prefix string `json:"prefix,omitempty"` + Prefix string `yaml:"prefix,omitempty"` // BasicAuthPassword converts this claim into a basic auth header. // Note the value of claim will become the basic auth username and the // basicAuthPassword will be used as the password value. - BasicAuthPassword *SecretSource `json:"basicAuthPassword,omitempty"` + BasicAuthPassword *SecretSource `yaml:"basicAuthPassword,omitempty"` } diff --git a/pkg/apis/options/hooks.go b/pkg/apis/options/hooks.go new file mode 100644 index 00000000..79653ecb --- /dev/null +++ b/pkg/apis/options/hooks.go @@ -0,0 +1,57 @@ +package options + +import ( + "reflect" + "time" + + "github.com/go-viper/mapstructure/v2" +) + +// Duration is an alias for time.Duration so that we can ensure the marshalling +// and unmarshalling of string durations is done as users expect. +// Intentional blank line below to keep this first part of the comment out of +// any generated references. + +// Duration is as string representation of a period of time. +// A duration string is a is a possibly signed sequence of decimal numbers, +// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + +// Conversion from string or floating point to golang duration type +// This way floating points will be converted to seconds and strings +// of type 3s or 5m will be parsed with time.ParseDuration +func toDurationHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if t != reflect.TypeOf(time.Duration(0)) { + return data, nil + } + + switch f.Kind() { + case reflect.String: + return time.ParseDuration(data.(string)) + case reflect.Float64: + return time.Duration(data.(float64) * float64(time.Second)), nil + case reflect.Int64: + return time.Duration(data.(int64)), nil + default: + return data, nil + } + } +} + +// StringToBytesHookFunc returns a DecodeHookFunc that converts string to []byte. +func stringToBytesHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}, + ) (interface{}, error) { + if f.Kind() == reflect.String && t == reflect.TypeOf([]byte{}) { + return []byte(data.(string)), nil + } + return data, nil + } +} diff --git a/pkg/apis/options/hooks_test.go b/pkg/apis/options/hooks_test.go new file mode 100644 index 00000000..99514a7c --- /dev/null +++ b/pkg/apis/options/hooks_test.go @@ -0,0 +1,96 @@ +package options + +import ( + "testing" + "time" +) + +func TestToDurationHook(t *testing.T) { + type result struct { + Duration time.Duration `yaml:"duration"` + } + + tests := []struct { + name string + input map[string]interface{} + out result + expected time.Duration + expectedErr bool + }{ + { + name: "Valid String Duration with single unit", + input: map[string]interface{}{"duration": "3s"}, + out: result{}, + expected: 3 * time.Second, + expectedErr: false, + }, + { + name: "Valid String Duration with multiple units", + input: map[string]interface{}{"duration": "1h20m30s"}, + out: result{}, + expected: 1*time.Hour + 20*time.Minute + 30*time.Second, + expectedErr: false, + }, + { + name: "Valid Float Duration", + input: map[string]interface{}{"duration": 2.5}, + out: result{}, + expected: 2500 * time.Millisecond, + expectedErr: false, + }, + { + name: "Valid Int64 Duration", + input: map[string]interface{}{"duration": int64(5000000000)}, + out: result{}, + expected: 5 * time.Second, + expectedErr: false, + }, + { + name: "Invalid String", + input: map[string]interface{}{"duration": "invalid"}, + out: result{}, + expected: 0, + expectedErr: true, + }, + { + name: "Unsupported Type", + input: map[string]interface{}{"duration": true}, + out: result{}, + expected: 0, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result struct { + Duration time.Duration `yaml:"duration"` + } + + err := Decode(tt.input, &result) + if (err != nil) != tt.expectedErr { + t.Errorf("expected error: %v, got: %v", tt.expectedErr, err) + } + + if !tt.expectedErr { + if result.Duration != tt.expected { + t.Errorf("expected: %v, got: %v", tt.expected, result.Duration) + } + } + }) + } +} + +func TestStringToBytesHook(t *testing.T) { + var result struct { + Value []byte `yaml:"value"` + } + + if err := Decode(map[string]interface{}{"value": "hello-world"}, &result); err != nil { + t.Fatal(err) + } + + if string(result.Value) != "hello-world" { + t.Errorf("expected %q, got %q", "hello-world", string(result.Value)) + } +} diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index 12975225..e70ae964 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -9,6 +9,7 @@ import ( "time" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" "github.com/spf13/pflag" ) @@ -136,18 +137,18 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) { u.Path = "/" } - flushInterval := Duration(l.FlushInterval) - timeout := Duration(l.Timeout) + flushInterval := l.FlushInterval + timeout := l.Timeout upstream := Upstream{ ID: u.Path, Path: u.Path, URI: upstreamString, - InsecureSkipTLSVerify: l.SSLUpstreamInsecureSkipVerify, + InsecureSkipTLSVerify: &l.SSLUpstreamInsecureSkipVerify, PassHostHeader: &l.PassHostHeader, ProxyWebSockets: &l.ProxyWebSockets, FlushInterval: &flushInterval, Timeout: &timeout, - DisableKeepAlives: l.DisableKeepAlives, + DisableKeepAlives: &l.DisableKeepAlives, } switch u.Scheme { @@ -164,7 +165,7 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) { logger.Errorf("unable to convert %q to int, use default \"200\"", u.Host) responseCode = 200 } - upstream.Static = true + upstream.Static = ptr.Ptr(true) upstream.StaticCode = &responseCode // This is not allowed to be empty and must be unique @@ -175,12 +176,12 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) { // Force defaults compatible with static responses upstream.URI = "" - upstream.InsecureSkipTLSVerify = false + upstream.InsecureSkipTLSVerify = ptr.Ptr(false) upstream.PassHostHeader = nil upstream.ProxyWebSockets = nil upstream.FlushInterval = nil upstream.Timeout = nil - upstream.DisableKeepAlives = false + upstream.DisableKeepAlives = ptr.Ptr(false) case "unix": upstream.Path = "/" } @@ -253,7 +254,7 @@ func (l *LegacyHeaders) getRequestHeaders() []Header { } for i := range requestHeaders { - requestHeaders[i].PreserveRequestValue = !l.SkipAuthStripHeaders + requestHeaders[i].PreserveRequestValue = ptr.Ptr(!l.SkipAuthStripHeaders) } return requestHeaders @@ -680,11 +681,11 @@ func (l *LegacyProvider) convert() (Providers, error) { ClientSecretFile: l.ClientSecretFile, Type: ProviderType(l.ProviderType), CAFiles: l.ProviderCAFiles, - UseSystemTrustStore: l.UseSystemTrustStore, + UseSystemTrustStore: &l.UseSystemTrustStore, LoginURL: l.LoginURL, RedeemURL: l.RedeemURL, ProfileURL: l.ProfileURL, - SkipClaimsFromProfileURL: l.SkipClaimsFromProfileURL, + SkipClaimsFromProfileURL: &l.SkipClaimsFromProfileURL, ProtectedResource: l.ProtectedResource, ValidateURL: l.ValidateURL, Scope: l.Scope, @@ -697,10 +698,10 @@ func (l *LegacyProvider) convert() (Providers, error) { // This part is out of the switch section for all providers that support OIDC provider.OIDCConfig = OIDCOptions{ IssuerURL: l.OIDCIssuerURL, - InsecureAllowUnverifiedEmail: l.InsecureOIDCAllowUnverifiedEmail, - InsecureSkipIssuerVerification: l.InsecureOIDCSkipIssuerVerification, - InsecureSkipNonce: l.InsecureOIDCSkipNonce, - SkipDiscovery: l.SkipOIDCDiscovery, + InsecureAllowUnverifiedEmail: &l.InsecureOIDCAllowUnverifiedEmail, + InsecureSkipIssuerVerification: &l.InsecureOIDCSkipIssuerVerification, + InsecureSkipNonce: &l.InsecureOIDCSkipNonce, + SkipDiscovery: &l.SkipOIDCDiscovery, JwksURL: l.OIDCJwksURL, UserIDClaim: l.UserIDClaim, EmailClaim: l.OIDCEmailClaim, @@ -768,13 +769,13 @@ func (l *LegacyProvider) convert() (Providers, error) { Groups: l.GoogleGroups, AdminEmail: l.GoogleAdminEmail, ServiceAccountJSON: l.GoogleServiceAccountJSON, - UseApplicationDefaultCredentials: l.GoogleUseApplicationDefaultCredentials, + UseApplicationDefaultCredentials: &l.GoogleUseApplicationDefaultCredentials, TargetPrincipal: l.GoogleTargetPrincipal, } case "entra-id": provider.MicrosoftEntraIDConfig = MicrosoftEntraIDOptions{ AllowedTenants: l.EntraIDAllowedTenants, - FederatedTokenAuth: l.EntraIDFederatedTokenAuth, + FederatedTokenAuth: &l.EntraIDFederatedTokenAuth, } } diff --git a/pkg/apis/options/legacy_options_test.go b/pkg/apis/options/legacy_options_test.go index 9481cf95..4348021b 100644 --- a/pkg/apis/options/legacy_options_test.go +++ b/pkg/apis/options/legacy_options_test.go @@ -3,6 +3,7 @@ package options import ( "time" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -15,8 +16,8 @@ var _ = Describe("Legacy Options", func() { legacyOpts := NewLegacyOptions() // Set upstreams and related options to test their conversion - flushInterval := Duration(5 * time.Second) - timeout := Duration(5 * time.Second) + flushInterval := 5 * time.Second + timeout := 5 * time.Second legacyOpts.LegacyUpstreams.FlushInterval = time.Duration(flushInterval) legacyOpts.LegacyUpstreams.Timeout = time.Duration(timeout) legacyOpts.LegacyUpstreams.PassHostHeader = true @@ -26,7 +27,6 @@ var _ = Describe("Legacy Options", func() { legacyOpts.LegacyProvider.ClientID = "oauth-proxy" legacyOpts.LegacyUpstreams.DisableKeepAlives = false - truth := true staticCode := 204 opts.UpstreamServers = UpstreamConfig{ Upstreams: []Upstream{ @@ -35,35 +35,35 @@ var _ = Describe("Legacy Options", func() { Path: "/baz", URI: "http://foo.bar/baz", FlushInterval: &flushInterval, - InsecureSkipTLSVerify: true, - PassHostHeader: &truth, - ProxyWebSockets: &truth, + InsecureSkipTLSVerify: ptr.Ptr(true), + PassHostHeader: ptr.Ptr(true), + ProxyWebSockets: ptr.Ptr(true), Timeout: &timeout, - DisableKeepAlives: legacyOpts.LegacyUpstreams.DisableKeepAlives, + DisableKeepAlives: &legacyOpts.LegacyUpstreams.DisableKeepAlives, }, { ID: "/bar", Path: "/bar", URI: "file:///var/lib/website", FlushInterval: &flushInterval, - InsecureSkipTLSVerify: true, - PassHostHeader: &truth, - ProxyWebSockets: &truth, + InsecureSkipTLSVerify: ptr.Ptr(true), + PassHostHeader: ptr.Ptr(true), + ProxyWebSockets: ptr.Ptr(true), Timeout: &timeout, - DisableKeepAlives: legacyOpts.LegacyUpstreams.DisableKeepAlives, + DisableKeepAlives: &legacyOpts.LegacyUpstreams.DisableKeepAlives, }, { ID: "static://204", Path: "/", URI: "", - Static: true, + Static: ptr.Ptr(true), StaticCode: &staticCode, FlushInterval: nil, - InsecureSkipTLSVerify: false, + InsecureSkipTLSVerify: ptr.Ptr(false), PassHostHeader: nil, ProxyWebSockets: nil, Timeout: nil, - DisableKeepAlives: legacyOpts.LegacyUpstreams.DisableKeepAlives, + DisableKeepAlives: &legacyOpts.LegacyUpstreams.DisableKeepAlives, }, }, } @@ -71,7 +71,7 @@ var _ = Describe("Legacy Options", func() { opts.InjectRequestHeaders = []Header{ { Name: "X-Forwarded-Groups", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -82,7 +82,7 @@ var _ = Describe("Legacy Options", func() { }, { Name: "X-Forwarded-User", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -93,7 +93,7 @@ var _ = Describe("Legacy Options", func() { }, { Name: "X-Forwarded-Email", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -104,7 +104,7 @@ var _ = Describe("Legacy Options", func() { }, { Name: "X-Forwarded-Preferred-Username", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -123,7 +123,7 @@ var _ = Describe("Legacy Options", func() { opts.Providers[0].ClientID = "oauth-proxy" opts.Providers[0].ID = "google=oauth-proxy" - opts.Providers[0].OIDCConfig.InsecureSkipNonce = true + opts.Providers[0].OIDCConfig.InsecureSkipNonce = ptr.Ptr(true) opts.Providers[0].OIDCConfig.AudienceClaims = []string{"aud"} opts.Providers[0].OIDCConfig.ExtraAudiences = []string{} opts.Providers[0].LoginURLParameters = []LoginURLParameter{ @@ -147,8 +147,8 @@ var _ = Describe("Legacy Options", func() { skipVerify := true passHostHeader := false proxyWebSockets := true - flushInterval := Duration(5 * time.Second) - timeout := Duration(5 * time.Second) + flushInterval := 5 * time.Second + timeout := 5 * time.Second disableKeepAlives := true // Test cases and expected outcomes @@ -157,12 +157,12 @@ var _ = Describe("Legacy Options", func() { ID: "/baz", Path: "/baz", URI: validHTTP, - InsecureSkipTLSVerify: skipVerify, + InsecureSkipTLSVerify: &skipVerify, PassHostHeader: &passHostHeader, ProxyWebSockets: &proxyWebSockets, FlushInterval: &flushInterval, Timeout: &timeout, - DisableKeepAlives: disableKeepAlives, + DisableKeepAlives: &disableKeepAlives, } // Test cases and expected outcomes @@ -171,12 +171,12 @@ var _ = Describe("Legacy Options", func() { ID: "/", Path: "/", URI: emptyPathHTTP, - InsecureSkipTLSVerify: skipVerify, + InsecureSkipTLSVerify: &skipVerify, PassHostHeader: &passHostHeader, ProxyWebSockets: &proxyWebSockets, FlushInterval: &flushInterval, Timeout: &timeout, - DisableKeepAlives: disableKeepAlives, + DisableKeepAlives: &disableKeepAlives, } validFileWithFragment := "file:///var/lib/website#/bar" @@ -184,12 +184,12 @@ var _ = Describe("Legacy Options", func() { ID: "/bar", Path: "/bar", URI: "file:///var/lib/website", - InsecureSkipTLSVerify: skipVerify, + InsecureSkipTLSVerify: &skipVerify, PassHostHeader: &passHostHeader, ProxyWebSockets: &proxyWebSockets, FlushInterval: &flushInterval, Timeout: &timeout, - DisableKeepAlives: disableKeepAlives, + DisableKeepAlives: &disableKeepAlives, } validStatic := "static://204" @@ -198,14 +198,14 @@ var _ = Describe("Legacy Options", func() { ID: validStatic, Path: "/", URI: "", - Static: true, + Static: ptr.Ptr(true), StaticCode: &validStaticCode, - InsecureSkipTLSVerify: false, + InsecureSkipTLSVerify: ptr.Ptr(false), PassHostHeader: nil, ProxyWebSockets: nil, FlushInterval: nil, Timeout: nil, - DisableKeepAlives: false, + DisableKeepAlives: ptr.Ptr(false), } invalidStatic := "static://abc" @@ -214,14 +214,14 @@ var _ = Describe("Legacy Options", func() { ID: invalidStatic, Path: "/", URI: "", - Static: true, + Static: ptr.Ptr(true), StaticCode: &invalidStaticCode, - InsecureSkipTLSVerify: false, + InsecureSkipTLSVerify: ptr.Ptr(false), PassHostHeader: nil, ProxyWebSockets: nil, FlushInterval: nil, Timeout: nil, - DisableKeepAlives: false, + DisableKeepAlives: ptr.Ptr(false), } invalidHTTP := ":foo" @@ -308,13 +308,13 @@ var _ = Describe("Legacy Options", func() { } withPreserveRequestValue := func(h Header, preserve bool) Header { - h.PreserveRequestValue = preserve + h.PreserveRequestValue = &preserve return h } xForwardedUser := Header{ Name: "X-Forwarded-User", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -326,7 +326,7 @@ var _ = Describe("Legacy Options", func() { xForwardedEmail := Header{ Name: "X-Forwarded-Email", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -338,7 +338,7 @@ var _ = Describe("Legacy Options", func() { xForwardedGroups := Header{ Name: "X-Forwarded-Groups", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -350,7 +350,7 @@ var _ = Describe("Legacy Options", func() { xForwardedPreferredUsername := Header{ Name: "X-Forwarded-Preferred-Username", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -362,7 +362,7 @@ var _ = Describe("Legacy Options", func() { basicAuthHeader := Header{ Name: "Authorization", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -378,7 +378,7 @@ var _ = Describe("Legacy Options", func() { xForwardedUserWithEmail := Header{ Name: "X-Forwarded-User", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -390,7 +390,7 @@ var _ = Describe("Legacy Options", func() { xForwardedAccessToken := Header{ Name: "X-Forwarded-Access-Token", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -402,7 +402,7 @@ var _ = Describe("Legacy Options", func() { basicAuthHeaderWithEmail := Header{ Name: "Authorization", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -418,7 +418,7 @@ var _ = Describe("Legacy Options", func() { xAuthRequestUser := Header{ Name: "X-Auth-Request-User", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -430,7 +430,7 @@ var _ = Describe("Legacy Options", func() { xAuthRequestEmail := Header{ Name: "X-Auth-Request-Email", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -442,7 +442,7 @@ var _ = Describe("Legacy Options", func() { xAuthRequestGroups := Header{ Name: "X-Auth-Request-Groups", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -454,7 +454,7 @@ var _ = Describe("Legacy Options", func() { xAuthRequestPreferredUsername := Header{ Name: "X-Auth-Request-Preferred-Username", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -466,7 +466,7 @@ var _ = Describe("Legacy Options", func() { xAuthRequestAccessToken := Header{ Name: "X-Auth-Request-Access-Token", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ @@ -478,7 +478,7 @@ var _ = Describe("Legacy Options", func() { authorizationHeader := Header{ Name: "Authorization", - PreserveRequestValue: false, + PreserveRequestValue: ptr.Ptr(false), Values: []HeaderValue{ { ClaimSource: &ClaimSource{ diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go index c302e8e7..fdd20c7a 100644 --- a/pkg/apis/options/load.go +++ b/pkg/apis/options/load.go @@ -9,10 +9,10 @@ import ( "strings" "github.com/a8m/envsubst" - "github.com/ghodss/yaml" "github.com/go-viper/mapstructure/v2" "github.com/spf13/pflag" "github.com/spf13/viper" + "gopkg.in/yaml.v3" ) // Load reads in the config file at the path given, then merges in environment @@ -55,6 +55,87 @@ func Load(configFileName string, flagSet *pflag.FlagSet, into interface{}) error return nil } +// LoadYAML will load a YAML based configuration file into the options interface provided. +func LoadYAML(configFileName string, opts interface{}) error { + buffer, err := loadAndSubstituteEnvs(configFileName) + if err != nil { + return err + } + + // Generic interface for loading arbitrary yaml structure + var intermediate map[string]interface{} + + if err := yaml.Unmarshal(buffer, &intermediate); err != nil { + return fmt.Errorf("error unmarshalling config: %w", err) + } + + // Using mapstructure to decode arbitrary yaml structure into options and + // merge with existing values instead of overwriting everything. This is especially + // important as we have a lot of default values for boolean which are supposed to be + // true by default. Normally by just parsing through yaml all booleans that aren't + // referenced in the config file would be parsed as false and we cannot identify after + // the fact if they have been explicitly set to false or have not been referenced. + return Decode(intermediate, opts) +} + +// Decode processes an input map and decodes it into a given struct while preserving default values. +// It ensures proper conversion of duration values from strings, floats, and int64 into time.Duration. +// +// Parameters: +// - input: A map[string]interface{} representing the input data. +// - result: A pointer to a struct where the decoded values will be stored. +// +// Returns: +// - An error if decoding fails or if there are unmapped keys. +func Decode(input interface{}, result interface{}) error { + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + toDurationHookFunc(), + stringToBytesHookFunc(), + ), + Metadata: nil, // Don't track any metadata + Result: result, // Decode the result into the prefilled options + TagName: "yaml", // Parse all fields that use the json tag + ZeroFields: false, // Don't clean the default values from the result map (options) + ErrorUnused: true, // Throw an error if keys have been used that aren't mapped to any struct fields + IgnoreUntaggedFields: true, // Ignore fields in structures that aren't tagged with json + }) + if err != nil { + return fmt.Errorf("error creating decoder for config: %w", err) + } + + if err := decoder.Decode(input); err != nil { + return fmt.Errorf("error decoding config: %w", err) + } + + return nil +} + +// loadAndSubstituteEnvs reads the yaml config into a generic byte buffer and +// substitute env references +func loadAndSubstituteEnvs(configFileName string) ([]byte, error) { + if configFileName == "" { + return nil, errors.New("no configuration file provided") + } + + unparsedBuffer, err := os.ReadFile(configFileName) + if err != nil { + return nil, fmt.Errorf("unable to load config file: %w", err) + } + + modifiedBuffer, err := normalizeSubstitution(unparsedBuffer) + if err != nil { + return nil, fmt.Errorf("error normalizing substitution string : %w", err) + } + + buffer, err := envsubst.Bytes(modifiedBuffer) + if err != nil { + return nil, fmt.Errorf("error in substituting env variables : %w", err) + } + + return buffer, nil +} + // registerFlags uses `cfg` and `flag` tags to associate flags in the flagSet // to the fields in the options interface provided. // Each exported field in the options must have a `cfg` tag otherwise an error will occur. @@ -140,47 +221,6 @@ func isUnexported(name string) bool { return first == strings.ToLower(first) } -// LoadYAML will load a YAML based configuration file into the options interface provided. -func LoadYAML(configFileName string, into interface{}) error { - buffer, err := loadAndParseYaml(configFileName) - if err != nil { - return err - } - - // UnmarshalStrict will return an error if the config includes options that are - // not mapped to fields of the into struct - if err := yaml.UnmarshalStrict(buffer, into, yaml.DisallowUnknownFields); err != nil { - return fmt.Errorf("error unmarshalling config: %w", err) - } - - return nil -} - -// loadAndParseYaml reads the config from the filesystem and -// execute the environment variable substitution -func loadAndParseYaml(configFileName string) ([]byte, error) { - if configFileName == "" { - return nil, errors.New("no configuration file provided") - } - - unparsedBuffer, err := os.ReadFile(configFileName) - if err != nil { - return nil, fmt.Errorf("unable to load config file: %w", err) - } - - modifiedBuffer, err := normalizeSubstitution(unparsedBuffer) - if err != nil { - return nil, fmt.Errorf("error normalizing substitution string : %w", err) - } - - buffer, err := envsubst.Bytes(modifiedBuffer) - if err != nil { - return nil, fmt.Errorf("error in substituting env variables : %w", err) - } - - return buffer, nil -} - // normalizeSubstitution normalizes dollar signs ($) with numerals like // $1 or $2 properly by correctly escaping them func normalizeSubstitution(unparsedBuffer []byte) ([]byte, error) { diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go index 06123c37..42083f76 100644 --- a/pkg/apis/options/load_test.go +++ b/pkg/apis/options/load_test.go @@ -155,7 +155,7 @@ var _ = Describe("Load", func() { } err := Load(configFileName, flagSet, input) if o.expectedErr != nil { - Expect(err).To(MatchError(o.expectedErr.Error())) + Expect(err).To(MatchError(ContainSubstring(o.expectedErr.Error()))) } else { Expect(err).ToNot(HaveOccurred()) } @@ -416,7 +416,7 @@ sub: err := LoadYAML(configFileName, input) if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) + Expect(err).To(MatchError(ContainSubstring(in.expectedErr.Error()))) } else { Expect(err).ToNot(HaveOccurred()) } @@ -445,7 +445,7 @@ sub: configFile: []byte("\tfoo: bar"), input: &TestOptions{}, expectedOutput: &TestOptions{}, - expectedErr: errors.New("error unmarshalling config: error converting YAML to JSON: yaml: found character that cannot start any token"), + expectedErr: errors.New("error unmarshalling config: yaml: found character that cannot start any token"), }), Entry("with extra fields in the YAML", loadYAMLTableInput{ configFile: append(testOptionsConfigBytesFull, []byte("foo: bar\n")...), @@ -459,19 +459,19 @@ sub: StringSliceOption: []string{"a", "b", "c"}, }, }, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""), + expectedErr: errors.New("has invalid keys: foo"), }), Entry("with an incorrect type for a string field", loadYAMLTableInput{ configFile: []byte(`stringOption: ["a", "b"]`), input: &TestOptions{}, expectedOutput: &TestOptions{}, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go struct field TestOptions.StringOption of type string"), + expectedErr: errors.New("'stringOption' expected type 'string', got unconvertible type"), }), Entry("with an incorrect type for an array field", loadYAMLTableInput{ configFile: []byte(`stringSliceOption: "a"`), input: &TestOptions{}, expectedOutput: &TestOptions{}, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field TestOptions.TestOptionSubStruct.StringSliceOption of type []string"), + expectedErr: errors.New("error decoding config: decoding failed due to the following error(s):\n\n'stringSliceOption' source data must be an array or slice, got string"), }), Entry("with a config file containing environment variable references", loadYAMLTableInput{ configFile: []byte("stringOption: ${TESTUSER}"), @@ -526,11 +526,13 @@ upstreamConfig: injectRequestHeaders: - name: X-Forwarded-User values: - - claim: user + - claimSource: + claim: user injectResponseHeaders: - name: X-Secret values: - - value: c2VjcmV0 + - secretSource: + value: secret `) By("Creating a config file") @@ -548,7 +550,7 @@ injectResponseHeaders: into := &AlphaOptions{} Expect(LoadYAML(configFileName, into)).To(Succeed()) - flushInterval := Duration(500 * time.Millisecond) + flushInterval := 500 * time.Millisecond Expect(into).To(Equal(&AlphaOptions{ UpstreamConfig: UpstreamConfig{ diff --git a/pkg/apis/options/login_url_parameters.go b/pkg/apis/options/login_url_parameters.go index 1cb763b9..02729760 100644 --- a/pkg/apis/options/login_url_parameters.go +++ b/pkg/apis/options/login_url_parameters.go @@ -71,19 +71,19 @@ package options // character. type LoginURLParameter struct { // Name specifies the name of the query parameter. - Name string `json:"name"` + Name string `yaml:"name"` // Default specifies a default value or values that will be // passed to the IdP if not overridden. //+optional - Default []string `json:"default,omitempty"` + Default []string `yaml:"default,omitempty"` // Allow specifies rules about how the default (if any) may be // overridden via the query string to `/oauth2/start`. Only // values that match one or more of the allow rules will be // forwarded to the IdP. //+optional - Allow []URLParameterRule `json:"allow,omitempty"` + Allow []URLParameterRule `yaml:"allow,omitempty"` } // URLParameterRule represents a rule by which query parameters @@ -92,11 +92,11 @@ type LoginURLParameter struct { // login URL. Either Value or Pattern should be supplied, not both. type URLParameterRule struct { // A Value rule matches just this specific value - Value *string `json:"value,omitempty"` + Value *string `yaml:"value,omitempty"` // A Pattern rule gives a regular expression that must be matched by // some substring of the value. The expression is _not_ automatically // anchored to the start and end of the value, if you _want_ to restrict // the whole parameter value you must anchor it yourself with `^` and `$`. - Pattern *string `json:"pattern,omitempty"` + Pattern *string `yaml:"pattern,omitempty"` } diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 0f254575..21f622dd 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -1,5 +1,7 @@ package options +import "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" + const ( // OIDCEmailClaim is the generic email claim used by the OIDC provider. OIDCEmailClaim = "email" @@ -22,78 +24,78 @@ type Providers []Provider type Provider struct { // ClientID is the OAuth Client ID that is defined in the provider // This value is required for all providers. - ClientID string `json:"clientID,omitempty"` + ClientID string `yaml:"clientID,omitempty"` // ClientSecret is the OAuth Client Secret that is defined in the provider // This value is required for all providers. - ClientSecret string `json:"clientSecret,omitempty"` + ClientSecret string `yaml:"clientSecret,omitempty"` // ClientSecretFile is the name of the file // containing the OAuth Client Secret, it will be used if ClientSecret is not set. - ClientSecretFile string `json:"clientSecretFile,omitempty"` + ClientSecretFile string `yaml:"clientSecretFile,omitempty"` // KeycloakConfig holds all configurations for Keycloak provider. - KeycloakConfig KeycloakOptions `json:"keycloakConfig,omitempty"` + KeycloakConfig KeycloakOptions `yaml:"keycloakConfig,omitempty"` // AzureConfig holds all configurations for Azure provider. - AzureConfig AzureOptions `json:"azureConfig,omitempty"` + AzureConfig AzureOptions `yaml:"azureConfig,omitempty"` // MicrosoftEntraIDConfig holds all configurations for Entra ID provider. - MicrosoftEntraIDConfig MicrosoftEntraIDOptions `json:"microsoftEntraIDConfig,omitempty"` + MicrosoftEntraIDConfig MicrosoftEntraIDOptions `yaml:"microsoftEntraIDConfig,omitempty"` // ADFSConfig holds all configurations for ADFS provider. - ADFSConfig ADFSOptions `json:"ADFSConfig,omitempty"` + ADFSConfig ADFSOptions `yaml:"ADFSConfig,omitempty"` // BitbucketConfig holds all configurations for Bitbucket provider. - BitbucketConfig BitbucketOptions `json:"bitbucketConfig,omitempty"` + BitbucketConfig BitbucketOptions `yaml:"bitbucketConfig,omitempty"` // GitHubConfig holds all configurations for GitHubC provider. - GitHubConfig GitHubOptions `json:"githubConfig,omitempty"` + GitHubConfig GitHubOptions `yaml:"githubConfig,omitempty"` // GitLabConfig holds all configurations for GitLab provider. - GitLabConfig GitLabOptions `json:"gitlabConfig,omitempty"` + GitLabConfig GitLabOptions `yaml:"gitlabConfig,omitempty"` // GoogleConfig holds all configurations for Google provider. - GoogleConfig GoogleOptions `json:"googleConfig,omitempty"` + GoogleConfig GoogleOptions `yaml:"googleConfig,omitempty"` // OIDCConfig holds all configurations for OIDC provider // or providers utilize OIDC configurations. - OIDCConfig OIDCOptions `json:"oidcConfig,omitempty"` + OIDCConfig OIDCOptions `yaml:"oidcConfig,omitempty"` // LoginGovConfig holds all configurations for LoginGov provider. - LoginGovConfig LoginGovOptions `json:"loginGovConfig,omitempty"` + LoginGovConfig LoginGovOptions `yaml:"loginGovConfig,omitempty"` // ID should be a unique identifier for the provider. // This value is required for all providers. - ID string `json:"id,omitempty"` + ID string `yaml:"id,omitempty"` // Type is the OAuth provider // must be set from the supported providers group, // otherwise 'Google' is set as default - Type ProviderType `json:"provider,omitempty"` + Type ProviderType `yaml:"provider,omitempty"` // Name is the providers display name // if set, it will be shown to the users in the login page. - Name string `json:"name,omitempty"` + Name string `yaml:"name,omitempty"` // CAFiles is a list of paths to CA certificates that should be used when connecting to the provider. // If not specified, the default Go trust sources are used instead - CAFiles []string `json:"caFiles,omitempty"` + CAFiles []string `yaml:"caFiles,omitempty"` // UseSystemTrustStore determines if your custom CA files and the system trust store are used // If set to true, your custom CA files and the system trust store are used otherwise only your custom CA files. - UseSystemTrustStore bool `json:"useSystemTrustStore,omitempty"` + UseSystemTrustStore *bool `yaml:"useSystemTrustStore,omitempty"` // LoginURL is the authentication endpoint - LoginURL string `json:"loginURL,omitempty"` + LoginURL string `yaml:"loginURL,omitempty"` // LoginURLParameters defines the parameters that can be passed from the start URL to the IdP login URL - LoginURLParameters []LoginURLParameter `json:"loginURLParameters,omitempty"` + LoginURLParameters []LoginURLParameter `yaml:"loginURLParameters,omitempty"` // AuthRequestResponseMode defines the response mode to request during authorization request - AuthRequestResponseMode string `json:"authRequestResponseMode,omitempty"` + AuthRequestResponseMode string `yaml:"authRequestResponseMode,omitempty"` // RedeemURL is the token redemption endpoint - RedeemURL string `json:"redeemURL,omitempty"` + RedeemURL string `yaml:"redeemURL,omitempty"` // ProfileURL is the profile access endpoint - ProfileURL string `json:"profileURL,omitempty"` + ProfileURL string `yaml:"profileURL,omitempty"` // SkipClaimsFromProfileURL allows to skip request to Profile URL for resolving claims not present in id_token // default set to 'false' - SkipClaimsFromProfileURL bool `json:"skipClaimsFromProfileURL,omitempty"` + SkipClaimsFromProfileURL *bool `yaml:"skipClaimsFromProfileURL,omitempty"` // ProtectedResource is the resource that is protected (Azure AD and ADFS only) - ProtectedResource string `json:"resource,omitempty"` + ProtectedResource string `yaml:"resource,omitempty"` // ValidateURL is the access token validation endpoint - ValidateURL string `json:"validateURL,omitempty"` + ValidateURL string `yaml:"validateURL,omitempty"` // Scope is the OAuth scope specification - Scope string `json:"scope,omitempty"` + Scope string `yaml:"scope,omitempty"` // AllowedGroups is a list of restrict logins to members of this group - AllowedGroups []string `json:"allowedGroups,omitempty"` + AllowedGroups []string `yaml:"allowedGroups,omitempty"` // The code challenge method - CodeChallengeMethod string `json:"code_challenge_method,omitempty"` + CodeChallengeMethod string `yaml:"code_challenge_method,omitempty"` // URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session - BackendLogoutURL string `json:"backendLogoutURL"` + BackendLogoutURL string `yaml:"backendLogoutURL"` } // ProviderType is used to enumerate the different provider type options @@ -157,19 +159,19 @@ const ( type KeycloakOptions struct { // Group enables to restrict login to members of indicated group - Groups []string `json:"groups,omitempty"` + Groups []string `yaml:"groups,omitempty"` // Role enables to restrict login to users with role (only available when using the keycloak-oidc provider) - Roles []string `json:"roles,omitempty"` + Roles []string `yaml:"roles,omitempty"` } type AzureOptions struct { // Tenant directs to a tenant-specific or common (tenant-independent) endpoint // Default value is 'common' - Tenant string `json:"tenant,omitempty"` + Tenant string `yaml:"tenant,omitempty"` // GraphGroupField configures the group field to be used when building the groups list from Microsoft Graph // Default value is 'id' - GraphGroupField string `json:"graphGroupField,omitempty"` + GraphGroupField string `yaml:"graphGroupField,omitempty"` } type MicrosoftEntraIDOptions struct { @@ -177,110 +179,110 @@ type MicrosoftEntraIDOptions struct { // issued by different issuers and OIDC issuer verification needs to be disabled. // When not specified, all tenants are allowed. Redundant for single-tenant apps // (regular ID token validation matches the issuer). - AllowedTenants []string `json:"allowedTenants,omitempty"` + AllowedTenants []string `yaml:"allowedTenants,omitempty"` // FederatedTokenAuth enable oAuth2 client authentication with federated token projected // by Entra Workload Identity plugin, instead of client secret. - FederatedTokenAuth bool `json:"federatedTokenAuth,omitempty"` + FederatedTokenAuth *bool `yaml:"federatedTokenAuth,omitempty"` } type ADFSOptions struct { // Skip adding the scope parameter in login request // Default value is 'false' - SkipScope bool `json:"skipScope,omitempty"` + SkipScope *bool `yaml:"skipScope,omitempty"` } type BitbucketOptions struct { // Team sets restrict logins to members of this team - Team string `json:"team,omitempty"` + Team string `yaml:"team,omitempty"` // Repository sets restrict logins to user with access to this repository - Repository string `json:"repository,omitempty"` + Repository string `yaml:"repository,omitempty"` } type GitHubOptions struct { // Org sets restrict logins to members of this organisation - Org string `json:"org,omitempty"` + Org string `yaml:"org,omitempty"` // Team sets restrict logins to members of this team - Team string `json:"team,omitempty"` + Team string `yaml:"team,omitempty"` // Repo sets restrict logins to collaborators of this repository - Repo string `json:"repo,omitempty"` + Repo string `yaml:"repo,omitempty"` // Token is the token to use when verifying repository collaborators // it must have push access to the repository - Token string `json:"token,omitempty"` + Token string `yaml:"token,omitempty"` // Users allows users with these usernames to login // even if they do not belong to the specified org and team or collaborators - Users []string `json:"users,omitempty"` + Users []string `yaml:"users,omitempty"` } type GitLabOptions struct { // Group sets restrict logins to members of this group - Group []string `json:"group,omitempty"` + Group []string `yaml:"group,omitempty"` // Projects restricts logins to members of these projects - Projects []string `json:"projects,omitempty"` + Projects []string `yaml:"projects,omitempty"` } type GoogleOptions struct { // Groups sets restrict logins to members of this Google group - Groups []string `json:"group,omitempty"` + Groups []string `yaml:"group,omitempty"` // AdminEmail is the Google admin to impersonate for api calls - AdminEmail string `json:"adminEmail,omitempty"` + AdminEmail string `yaml:"adminEmail,omitempty"` // ServiceAccountJSON is the path to the service account json credentials - ServiceAccountJSON string `json:"serviceAccountJson,omitempty"` + ServiceAccountJSON string `yaml:"serviceAccountJson,omitempty"` // UseApplicationDefaultCredentials is a boolean whether to use Application Default Credentials instead of a ServiceAccountJSON - UseApplicationDefaultCredentials bool `json:"useApplicationDefaultCredentials,omitempty"` + UseApplicationDefaultCredentials *bool `yaml:"useApplicationDefaultCredentials,omitempty"` // TargetPrincipal is the Google Service Account used for Application Default Credentials - TargetPrincipal string `json:"targetPrincipal,omitempty"` + TargetPrincipal string `yaml:"targetPrincipal,omitempty"` } type OIDCOptions struct { // IssuerURL is the OpenID Connect issuer URL // eg: https://accounts.google.com - IssuerURL string `json:"issuerURL,omitempty"` + IssuerURL string `yaml:"issuerURL,omitempty"` // InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified // default set to 'false' - InsecureAllowUnverifiedEmail bool `json:"insecureAllowUnverifiedEmail,omitempty"` + InsecureAllowUnverifiedEmail *bool `yaml:"insecureAllowUnverifiedEmail,omitempty"` // InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL // default set to 'false' - InsecureSkipIssuerVerification bool `json:"insecureSkipIssuerVerification,omitempty"` + InsecureSkipIssuerVerification *bool `yaml:"insecureSkipIssuerVerification,omitempty"` // InsecureSkipNonce skips verifying the ID Token's nonce claim that must match // the random nonce sent in the initial OAuth flow. Otherwise, the nonce is checked // after the initial OAuth redeem & subsequent token refreshes. // default set to 'true' // Warning: In a future release, this will change to 'false' by default for enhanced security. - InsecureSkipNonce bool `json:"insecureSkipNonce,omitempty"` + InsecureSkipNonce *bool `yaml:"insecureSkipNonce,omitempty"` // SkipDiscovery allows to skip OIDC discovery and use manually supplied Endpoints // default set to 'false' - SkipDiscovery bool `json:"skipDiscovery,omitempty"` + SkipDiscovery *bool `yaml:"skipDiscovery,omitempty"` // JwksURL is the OpenID Connect JWKS URL // eg: https://www.googleapis.com/oauth2/v3/certs - JwksURL string `json:"jwksURL,omitempty"` + JwksURL string `yaml:"jwksURL,omitempty"` // PublicKeyFiles is a list of paths pointing to public key files in PEM format to use // for verifying JWT tokens - PublicKeyFiles []string `json:"publicKeyFiles,omitempty"` + PublicKeyFiles []string `yaml:"publicKeyFiles,omitempty"` // EmailClaim indicates which claim contains the user email, // default set to 'email' - EmailClaim string `json:"emailClaim,omitempty"` + EmailClaim string `yaml:"emailClaim,omitempty"` // GroupsClaim indicates which claim contains the user groups // default set to 'groups' - GroupsClaim string `json:"groupsClaim,omitempty"` + GroupsClaim string `yaml:"groupsClaim,omitempty"` // UserIDClaim indicates which claim contains the user ID // default set to 'email' - UserIDClaim string `json:"userIDClaim,omitempty"` + UserIDClaim string `yaml:"userIDClaim,omitempty"` // AudienceClaim allows to define any claim that is verified against the client id // By default `aud` claim is used for verification. - AudienceClaims []string `json:"audienceClaims,omitempty"` + AudienceClaims []string `yaml:"audienceClaims,omitempty"` // ExtraAudiences is a list of additional audiences that are allowed // to pass verification in addition to the client id. - ExtraAudiences []string `json:"extraAudiences,omitempty"` + ExtraAudiences []string `yaml:"extraAudiences,omitempty"` } type LoginGovOptions struct { // JWTKey is a private key in PEM format used to sign JWT, - JWTKey string `json:"jwtKey,omitempty"` + JWTKey string `yaml:"jwtKey,omitempty"` // JWTKeyFile is a path to the private key file in PEM format used to sign the JWT - JWTKeyFile string `json:"jwtKeyFile,omitempty"` + JWTKeyFile string `yaml:"jwtKeyFile,omitempty"` // PubJWKURL is the JWK pubkey access endpoint - PubJWKURL string `json:"pubjwkURL,omitempty"` + PubJWKURL string `yaml:"pubjwkURL,omitempty"` } func providerDefaults() Providers { @@ -291,9 +293,9 @@ func providerDefaults() Providers { Tenant: "common", }, OIDCConfig: OIDCOptions{ - InsecureAllowUnverifiedEmail: false, - InsecureSkipNonce: true, - SkipDiscovery: false, + InsecureAllowUnverifiedEmail: ptr.Ptr(false), + InsecureSkipNonce: ptr.Ptr(true), + SkipDiscovery: ptr.Ptr(false), UserIDClaim: OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim EmailClaim: OIDCEmailClaim, GroupsClaim: OIDCGroupsClaim, diff --git a/pkg/apis/options/secret_source.go b/pkg/apis/options/secret_source.go new file mode 100644 index 00000000..848f1635 --- /dev/null +++ b/pkg/apis/options/secret_source.go @@ -0,0 +1,14 @@ +package options + +// SecretSource references an individual secret value. +// Only one source within the struct should be defined at any time. +type SecretSource struct { + // Value expects a base64 encoded string value. + Value []byte `yaml:"value,omitempty"` + + // FromEnv expects the name of an environment variable. + FromEnv string `yaml:"fromEnv,omitempty"` + + // FromFile expects a path to a file containing the secret value. + FromFile string `yaml:"fromFile,omitempty"` +} diff --git a/pkg/apis/options/server.go b/pkg/apis/options/server.go index f423ef2c..8fa41af8 100644 --- a/pkg/apis/options/server.go +++ b/pkg/apis/options/server.go @@ -4,15 +4,15 @@ package options type Server struct { // BindAddress is the address on which to serve traffic. // Leave blank or set to "-" to disable. - BindAddress string + BindAddress string `yaml:"bindAddress,omitempty"` // SecureBindAddress is the address on which to serve secure traffic. // Leave blank or set to "-" to disable. - SecureBindAddress string + SecureBindAddress string `yaml:"secureBindAddress,omitempty"` // TLS contains the information for loading the certificate and key for the // secure traffic and further configuration for the TLS server. - TLS *TLS + TLS *TLS `yaml:"tls,omitempty"` } // TLS contains the information for loading a TLS certificate and key @@ -20,15 +20,15 @@ type Server struct { type TLS struct { // Key is the TLS key data to use. // Typically this will come from a file. - Key *SecretSource + Key *SecretSource `yaml:"key,omitempty"` // Cert is the TLS certificate data to use. // Typically this will come from a file. - Cert *SecretSource + Cert *SecretSource `yaml:"cert,omitempty"` // MinVersion is the minimal TLS version that is acceptable. // E.g. Set to "TLS1.3" to select TLS version 1.3 - MinVersion string + MinVersion string `yaml:"minVersion,omitempty"` // CipherSuites is a list of TLS cipher suites that are allowed. // E.g.: @@ -36,5 +36,5 @@ type TLS struct { // - TLS_RSA_WITH_AES_256_GCM_SHA384 // If not specified, the default Go safe cipher list is used. // List of valid cipher suites can be found in the [crypto/tls documentation](https://pkg.go.dev/crypto/tls#pkg-constants). - CipherSuites []string + CipherSuites []string `yaml:"cipherSuites,omitempty"` } diff --git a/pkg/apis/options/upstreams.go b/pkg/apis/options/upstreams.go index b3c7195f..f12a4bf3 100644 --- a/pkg/apis/options/upstreams.go +++ b/pkg/apis/options/upstreams.go @@ -14,11 +14,11 @@ const ( type UpstreamConfig struct { // ProxyRawPath will pass the raw url path to upstream allowing for urls // like: "/%2F/" which would otherwise be redirected to "/" - ProxyRawPath bool `json:"proxyRawPath,omitempty"` + ProxyRawPath *bool `yaml:"proxyRawPath,omitempty"` // Upstreams represents the configuration for the upstream servers. // Requests will be proxied to this upstream if the path matches the request path. - Upstreams []Upstream `json:"upstreams,omitempty"` + Upstreams []Upstream `yaml:"upstreams,omitempty"` } // Upstream represents the configuration for an upstream server. @@ -26,7 +26,7 @@ type UpstreamConfig struct { type Upstream struct { // ID should be a unique identifier for the upstream. // This value is required for all upstreams. - ID string `json:"id,omitempty"` + ID string `yaml:"id,omitempty"` // Path is used to map requests to the upstream server. // The closest match will take precedence and all Paths must be unique. @@ -36,7 +36,7 @@ type Upstream struct { // - `^/foo$`: Match only the explicit path `/foo` // - `^/bar/$`: Match any path prefixed with `/bar/` // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget - Path string `json:"path,omitempty"` + Path string `yaml:"path,omitempty"` // RewriteTarget allows users to rewrite the request path before it is sent to // the upstream server (for an HTTP/HTTPS upstream) or mapped to the filesystem @@ -46,7 +46,7 @@ type Upstream struct { // the request `/baz/abc/123` to `/foo/abc/123` before proxying to the // upstream server. Or if the upstream were `file:///app`, a request for // `/baz/info.html` would return the contents of the file `/app/foo/info.html`. - RewriteTarget string `json:"rewriteTarget,omitempty"` + RewriteTarget string `yaml:"rewriteTarget,omitempty"` // The URI of the upstream server. This may be an HTTP(S) server of a File // based URL. It may include a path, in which case all requests will be served @@ -58,43 +58,43 @@ type Upstream struct { // - file://host/path // If the URI's path is "/base" and the incoming request was for "/dir", // the upstream request will be for "/base/dir". - URI string `json:"uri,omitempty"` + URI string `yaml:"uri,omitempty"` // InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts. // This option is insecure and will allow potential Man-In-The-Middle attacks // between OAuth2 Proxy and the upstream server. // Defaults to false. - InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` + InsecureSkipTLSVerify *bool `yaml:"insecureSkipTLSVerify,omitempty"` // Static will make all requests to this upstream have a static response. // The response will have a body of "Authenticated" and a response code // matching StaticCode. // If StaticCode is not set, the response will return a 200 response. - Static bool `json:"static,omitempty"` + Static *bool `yaml:"static,omitempty"` // StaticCode determines the response code for the Static response. // This option can only be used with Static enabled. - StaticCode *int `json:"staticCode,omitempty"` + StaticCode *int `yaml:"staticCode,omitempty"` // FlushInterval is the period between flushing the response buffer when // streaming response from the upstream. // Defaults to 1 second. - FlushInterval *Duration `json:"flushInterval,omitempty"` + FlushInterval *time.Duration `yaml:"flushInterval,omitempty"` // PassHostHeader determines whether the request host header should be proxied // to the upstream server. // Defaults to true. - PassHostHeader *bool `json:"passHostHeader,omitempty"` + PassHostHeader *bool `yaml:"passHostHeader,omitempty"` // ProxyWebSockets enables proxying of websockets to upstream servers // Defaults to true. - ProxyWebSockets *bool `json:"proxyWebSockets,omitempty"` + ProxyWebSockets *bool `yaml:"proxyWebSockets,omitempty"` // Timeout is the maximum duration the server will wait for a response from the upstream server. // Defaults to 30 seconds. - Timeout *Duration `json:"timeout,omitempty"` + Timeout *time.Duration `yaml:"timeout,omitempty"` // DisableKeepAlives disables HTTP keep-alive connections to the upstream server. // Defaults to false. - DisableKeepAlives bool `json:"disableKeepAlives,omitempty"` + DisableKeepAlives *bool `yaml:"disableKeepAlives,omitempty"` } diff --git a/pkg/middleware/headers.go b/pkg/middleware/headers.go index 8d2f8e3e..3fe56d42 100644 --- a/pkg/middleware/headers.go +++ b/pkg/middleware/headers.go @@ -27,7 +27,7 @@ func NewRequestHeaderInjector(headers []options.Header) (alice.Constructor, erro func newStripHeaders(headers []options.Header) alice.Constructor { headersToStrip := []string{} for _, header := range headers { - if !header.PreserveRequestValue { + if !(*header.PreserveRequestValue) { headersToStrip = append(headersToStrip, header.Name) } } diff --git a/pkg/middleware/headers_test.go b/pkg/middleware/headers_test.go index 06440eea..c05a732f 100644 --- a/pkg/middleware/headers_test.go +++ b/pkg/middleware/headers_test.go @@ -8,6 +8,7 @@ import ( middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -115,7 +116,7 @@ var _ = Describe("Headers Suite", func() { headers: []options.Header{ { Name: "Claim", - PreserveRequestValue: true, + PreserveRequestValue: ptr.Ptr(true), Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ @@ -160,7 +161,7 @@ var _ = Describe("Headers Suite", func() { headers: []options.Header{ { Name: "Claim", - PreserveRequestValue: true, + PreserveRequestValue: ptr.Ptr(true), Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ @@ -341,7 +342,7 @@ var _ = Describe("Headers Suite", func() { headers: []options.Header{ { Name: "Claim", - PreserveRequestValue: true, + PreserveRequestValue: ptr.Ptr(true), Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ @@ -388,7 +389,7 @@ var _ = Describe("Headers Suite", func() { headers: []options.Header{ { Name: "Claim", - PreserveRequestValue: true, + PreserveRequestValue: ptr.Ptr(true), Values: []options.HeaderValue{ { ClaimSource: &options.ClaimSource{ diff --git a/pkg/requests/result_test.go b/pkg/requests/result_test.go index b6ecee74..76073975 100644 --- a/pkg/requests/result_test.go +++ b/pkg/requests/result_test.go @@ -104,8 +104,8 @@ var _ = Describe("Result suite", func() { Context("UnmarshalInto", func() { type testStruct struct { - A string `json:"a"` - B int `json:"b"` + A string `yaml:"a"` + B int `yaml:"b"` } type unmarshalIntoTableInput struct { diff --git a/pkg/upstream/http.go b/pkg/upstream/http.go index 7a0e6e84..9f0d084c 100644 --- a/pkg/upstream/http.go +++ b/pkg/upstream/http.go @@ -54,7 +54,7 @@ func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *option // Set up a WebSocket proxy if required var wsProxy http.Handler if upstream.ProxyWebSockets == nil || *upstream.ProxyWebSockets { - wsProxy = newWebSocketReverseProxy(u, upstream.InsecureSkipTLSVerify) + wsProxy = newWebSocketReverseProxy(u, *upstream.InsecureSkipTLSVerify) } var auth hmacauth.HmacAuth @@ -137,19 +137,19 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr // Change default duration for waiting for an upstream response if upstream.Timeout != nil { - transport.ResponseHeaderTimeout = upstream.Timeout.Duration() + transport.ResponseHeaderTimeout = *upstream.Timeout } // Configure options on the SingleHostReverseProxy if upstream.FlushInterval != nil { - proxy.FlushInterval = upstream.FlushInterval.Duration() + proxy.FlushInterval = *upstream.FlushInterval } else { proxy.FlushInterval = options.DefaultUpstreamFlushInterval } // InsecureSkipVerify is a configurable option we allow /* #nosec G402 */ - if upstream.InsecureSkipTLSVerify { + if *upstream.InsecureSkipTLSVerify { transport.TLSClientConfig.InsecureSkipVerify = true } @@ -168,7 +168,7 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr // Pass on DisableKeepAlives to the transport settings // to allow for disabling HTTP keep-alive connections - transport.DisableKeepAlives = upstream.DisableKeepAlives + transport.DisableKeepAlives = *upstream.DisableKeepAlives // Apply the customized transport to our proxy before returning it proxy.Transport = transport diff --git a/pkg/upstream/http_test.go b/pkg/upstream/http_test.go index 31476df0..7c1831bf 100644 --- a/pkg/upstream/http_test.go +++ b/pkg/upstream/http_test.go @@ -15,16 +15,15 @@ import ( middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "golang.org/x/net/websocket" ) var _ = Describe("HTTP Upstream Suite", func() { - defaultFlushInterval := options.Duration(options.DefaultUpstreamFlushInterval) - defaultTimeout := options.Duration(options.DefaultUpstreamTimeout) - truth := true - falsum := false + defaultFlushInterval := options.DefaultUpstreamFlushInterval + defaultTimeout := options.DefaultUpstreamTimeout type httpUpstreamTableInput struct { id string @@ -57,15 +56,15 @@ var _ = Describe("HTTP Upstream Suite", func() { req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{}) rw := httptest.NewRecorder() - flush := options.Duration(1 * time.Second) + flush := 1 * time.Second - timeout := options.Duration(options.DefaultUpstreamTimeout) + timeout := options.DefaultUpstreamTimeout upstream := options.Upstream{ ID: in.id, PassHostHeader: &in.passUpstreamHostHeader, - ProxyWebSockets: &falsum, - InsecureSkipTLSVerify: false, + ProxyWebSockets: ptr.Ptr(false), + InsecureSkipTLSVerify: ptr.Ptr(false), FlushInterval: &flush, Timeout: &timeout, } @@ -343,9 +342,9 @@ var _ = Describe("HTTP Upstream Suite", func() { upstream := options.Upstream{ ID: "noPassHost", - PassHostHeader: &falsum, - ProxyWebSockets: &falsum, - InsecureSkipTLSVerify: false, + PassHostHeader: ptr.Ptr(false), + ProxyWebSockets: ptr.Ptr(false), + InsecureSkipTLSVerify: ptr.Ptr(false), FlushInterval: &defaultFlushInterval, Timeout: &defaultTimeout, } @@ -373,11 +372,11 @@ var _ = Describe("HTTP Upstream Suite", func() { type newUpstreamTableInput struct { proxyWebSockets bool - flushInterval options.Duration + flushInterval time.Duration skipVerify bool sigData *options.SignatureData errorHandler func(http.ResponseWriter, *http.Request, error) - timeout options.Duration + timeout time.Duration disableKeepAlives bool } @@ -389,10 +388,10 @@ var _ = Describe("HTTP Upstream Suite", func() { upstream := options.Upstream{ ID: "foo123", FlushInterval: &in.flushInterval, - InsecureSkipTLSVerify: in.skipVerify, + InsecureSkipTLSVerify: &in.skipVerify, ProxyWebSockets: &in.proxyWebSockets, Timeout: &in.timeout, - DisableKeepAlives: in.disableKeepAlives, + DisableKeepAlives: &in.disableKeepAlives, } handler := newHTTPUpstreamProxy(upstream, u, in.sigData, in.errorHandler) @@ -406,10 +405,10 @@ var _ = Describe("HTTP Upstream Suite", func() { proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy) Expect(ok).To(BeTrue()) - Expect(proxy.FlushInterval).To(Equal(in.flushInterval.Duration())) + Expect(proxy.FlushInterval).To(Equal(in.flushInterval)) transport, ok := proxy.Transport.(*http.Transport) Expect(ok).To(BeTrue()) - Expect(transport.ResponseHeaderTimeout).To(Equal(in.timeout.Duration())) + Expect(transport.ResponseHeaderTimeout).To(Equal(in.timeout)) Expect(proxy.ErrorHandler != nil).To(Equal(in.errorHandler != nil)) if in.skipVerify { Expect(transport.TLSClientConfig.InsecureSkipVerify).To(Equal(true)) @@ -428,7 +427,7 @@ var _ = Describe("HTTP Upstream Suite", func() { }), Entry("with a non standard flush interval", &newUpstreamTableInput{ proxyWebSockets: false, - flushInterval: options.Duration(5 * time.Second), + flushInterval: 5 * time.Second, skipVerify: false, sigData: nil, errorHandler: nil, @@ -466,7 +465,7 @@ var _ = Describe("HTTP Upstream Suite", func() { skipVerify: false, sigData: nil, errorHandler: nil, - timeout: options.Duration(5 * time.Second), + timeout: 5 * time.Second, }), Entry("with a DisableKeepAlives", &newUpstreamTableInput{ proxyWebSockets: false, @@ -483,13 +482,13 @@ var _ = Describe("HTTP Upstream Suite", func() { var proxyServer *httptest.Server BeforeEach(func() { - flush := options.Duration(1 * time.Second) - timeout := options.Duration(options.DefaultUpstreamTimeout) + flush := 1 * time.Second + timeout := options.DefaultUpstreamTimeout upstream := options.Upstream{ ID: "websocketProxy", - PassHostHeader: &truth, - ProxyWebSockets: &truth, - InsecureSkipTLSVerify: false, + PassHostHeader: ptr.Ptr(true), + ProxyWebSockets: ptr.Ptr(true), + InsecureSkipTLSVerify: ptr.Ptr(false), FlushInterval: &flush, Timeout: &timeout, } diff --git a/pkg/upstream/proxy.go b/pkg/upstream/proxy.go index 74b0d02d..0d2286ea 100644 --- a/pkg/upstream/proxy.go +++ b/pkg/upstream/proxy.go @@ -27,12 +27,12 @@ func NewProxy(upstreams options.UpstreamConfig, sigData *options.SignatureData, serveMux: mux.NewRouter(), } - if upstreams.ProxyRawPath { + if *upstreams.ProxyRawPath { m.serveMux.UseEncodedPath() } for _, upstream := range sortByPathLongest(upstreams.Upstreams) { - if upstream.Static { + if *upstream.Static { if err := m.registerStaticResponseHandler(upstream, writer); err != nil { return nil, fmt.Errorf("could not register static upstream %q: %v", upstream.ID, err) } diff --git a/pkg/upstream/proxy_test.go b/pkg/upstream/proxy_test.go index 5252bd42..87aae7fa 100644 --- a/pkg/upstream/proxy_test.go +++ b/pkg/upstream/proxy_test.go @@ -10,6 +10,7 @@ import ( middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -60,19 +61,19 @@ var _ = Describe("Proxy Suite", func() { { ID: "static-backend", Path: "/static/", - Static: true, + Static: ptr.Ptr(true), StaticCode: &ok, }, { ID: "static-backend-no-trailing-slash", Path: "/static", - Static: true, + Static: ptr.Ptr(true), StaticCode: &accepted, }, { ID: "static-backend-long", Path: "/static/long", - Static: true, + Static: ptr.Ptr(true), StaticCode: &accepted, }, { @@ -83,7 +84,7 @@ var _ = Describe("Proxy Suite", func() { { ID: "single-path-backend", Path: "/single-path", - Static: true, + Static: ptr.Ptr(true), StaticCode: &ok, }, { @@ -346,7 +347,7 @@ var _ = Describe("Proxy Suite", func() { upstream: "", }), Entry("containing an escaped '/' with ProxyRawPath", &proxyTableInput{ - upstreams: options.UpstreamConfig{ProxyRawPath: true}, + upstreams: options.UpstreamConfig{ProxyRawPath: ptr.Ptr(true)}, target: "http://example.localhost/%2F/test1/%2F/test2", response: testHTTPResponse{ code: 404, diff --git a/pkg/util/ptr/ptr.go b/pkg/util/ptr/ptr.go new file mode 100644 index 00000000..9242773c --- /dev/null +++ b/pkg/util/ptr/ptr.go @@ -0,0 +1,14 @@ +package ptr + +// Ptr generically returns a pointer to the given value. +func Ptr[T any](v T) *T { + return &v +} + +// Deref returns the value of the pointer or def(ault) if nil. +func Deref[T any](p *T, def T) T { + if p == nil { + return def + } + return *p +} diff --git a/pkg/util/ptr/ptr_test.go b/pkg/util/ptr/ptr_test.go new file mode 100644 index 00000000..c4817a6b --- /dev/null +++ b/pkg/util/ptr/ptr_test.go @@ -0,0 +1,38 @@ +package ptr + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPtr(t *testing.T) { + p := Ptr(42) + assert.NotNil(t, p) + assert.Equal(t, 42, *p) + + s := Ptr("hello") + assert.NotNil(t, s) + assert.Equal(t, "hello", *s) + + b := Ptr(true) + assert.NotNil(t, b) + assert.True(t, *b) +} + +func TestDeref(t *testing.T) { + v := Deref(Ptr(99), 0) + assert.Equal(t, 99, v) + + v = Deref[int](nil, 123) + assert.Equal(t, 123, v) + + s := Deref[string](nil, "default") + assert.Equal(t, "default", s) + + b := Deref(Ptr(true), false) + assert.True(t, b) + + b = Deref[bool](nil, false) + assert.False(t, b) +} diff --git a/pkg/validation/options.go b/pkg/validation/options.go index c720f47e..cd51c40c 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -34,7 +34,7 @@ func Validate(o *options.Options) error { transport := requests.DefaultTransport.(*http.Transport) transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 -- InsecureSkipVerify is a configurable option we allow } else if len(o.Providers[0].CAFiles) > 0 { - pool, err := util.GetCertPool(o.Providers[0].CAFiles, o.Providers[0].UseSystemTrustStore) + pool, err := util.GetCertPool(o.Providers[0].CAFiles, *o.Providers[0].UseSystemTrustStore) if err == nil { transport := requests.DefaultTransport.(*http.Transport) transport.TLSClientConfig = &tls.Config{ diff --git a/pkg/validation/options_test.go b/pkg/validation/options_test.go index 5c283545..6657e847 100644 --- a/pkg/validation/options_test.go +++ b/pkg/validation/options_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" "github.com/stretchr/testify/assert" ) @@ -68,7 +69,7 @@ func TestGoogleGroupOptionsWithoutServiceAccountJSON(t *testing.T) { func TestGoogleGroupOptionsWithoutAdminEmail(t *testing.T) { o := testOptions() - o.Providers[0].GoogleConfig.UseApplicationDefaultCredentials = true + o.Providers[0].GoogleConfig.UseApplicationDefaultCredentials = ptr.Ptr(true) err := Validate(o) assert.NotEqual(t, nil, err) @@ -81,7 +82,7 @@ func TestGoogleGroupOptionsWithoutGroups(t *testing.T) { o := testOptions() // Set admin email and application default credentials but no groups - should still require them o.Providers[0].GoogleConfig.AdminEmail = "admin@example.com" - o.Providers[0].GoogleConfig.UseApplicationDefaultCredentials = true + o.Providers[0].GoogleConfig.UseApplicationDefaultCredentials = ptr.Ptr(true) err := Validate(o) // Should pass validation since google-group is now optional assert.Equal(t, nil, err) diff --git a/pkg/validation/providers.go b/pkg/validation/providers.go index 4527b841..3abf69d4 100644 --- a/pkg/validation/providers.go +++ b/pkg/validation/providers.go @@ -5,6 +5,7 @@ import ( "os" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" ) // validateProviders is the initial validation migration for multiple providrers @@ -64,7 +65,7 @@ func validateProvider(provider options.Provider, providerIDs map[string]struct{} // providerRequiresClientSecret checks if provider requires client secret to be set // or it can be omitted in favor of JWT token to authenticate oAuth client func providerRequiresClientSecret(provider options.Provider) bool { - if provider.Type == "entra-id" && provider.MicrosoftEntraIDConfig.FederatedTokenAuth { + if provider.Type == "entra-id" && *provider.MicrosoftEntraIDConfig.FederatedTokenAuth { return false } @@ -96,7 +97,7 @@ func validateGoogleConfig(provider options.Provider) []string { hasAdminEmail := provider.GoogleConfig.AdminEmail != "" hasSAJSON := provider.GoogleConfig.ServiceAccountJSON != "" - useADC := provider.GoogleConfig.UseApplicationDefaultCredentials + useADC := ptr.Deref(provider.GoogleConfig.UseApplicationDefaultCredentials, false) if !hasAdminEmail && !hasSAJSON && !useADC { return msgs @@ -123,7 +124,7 @@ func validateGoogleConfig(provider options.Provider) []string { func validateEntraConfig(provider options.Provider) []string { msgs := []string{} - if provider.MicrosoftEntraIDConfig.FederatedTokenAuth { + if *provider.MicrosoftEntraIDConfig.FederatedTokenAuth { federatedTokenPath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE") if federatedTokenPath == "" { diff --git a/pkg/validation/upstreams.go b/pkg/validation/upstreams.go index bafed628..05d441e3 100644 --- a/pkg/validation/upstreams.go +++ b/pkg/validation/upstreams.go @@ -54,22 +54,22 @@ func validateUpstream(upstream options.Upstream, ids, paths map[string]struct{}) func validateStaticUpstream(upstream options.Upstream) []string { msgs := []string{} - if !upstream.Static && upstream.StaticCode != nil { + if !*upstream.Static && upstream.StaticCode != nil { msgs = append(msgs, fmt.Sprintf("upstream %q has staticCode (%d), but is not a static upstream, set 'static' for a static response", upstream.ID, *upstream.StaticCode)) } // Checks after this only make sense when the upstream is static - if !upstream.Static { + if !*upstream.Static { return msgs } if upstream.URI != "" { msgs = append(msgs, fmt.Sprintf("upstream %q has uri, but is a static upstream, this will have no effect.", upstream.ID)) } - if upstream.InsecureSkipTLSVerify { + if *upstream.InsecureSkipTLSVerify { msgs = append(msgs, fmt.Sprintf("upstream %q has insecureSkipTLSVerify, but is a static upstream, this will have no effect.", upstream.ID)) } - if upstream.FlushInterval != nil && upstream.FlushInterval.Duration() != options.DefaultUpstreamFlushInterval { + if upstream.FlushInterval != nil && *upstream.FlushInterval != options.DefaultUpstreamFlushInterval { msgs = append(msgs, fmt.Sprintf("upstream %q has flushInterval, but is a static upstream, this will have no effect.", upstream.ID)) } if upstream.PassHostHeader != nil { @@ -85,13 +85,13 @@ func validateStaticUpstream(upstream options.Upstream) []string { func validateUpstreamURI(upstream options.Upstream) []string { msgs := []string{} - if !upstream.Static && upstream.URI == "" { + if !*upstream.Static && upstream.URI == "" { msgs = append(msgs, fmt.Sprintf("upstream %q has empty uri: uris are required for all non-static upstreams", upstream.ID)) return msgs } // Checks after this only make sense the upstream is not static - if upstream.Static { + if *upstream.Static { return msgs } diff --git a/pkg/validation/upstreams_test.go b/pkg/validation/upstreams_test.go index fe431d27..580e2f29 100644 --- a/pkg/validation/upstreams_test.go +++ b/pkg/validation/upstreams_test.go @@ -4,6 +4,7 @@ import ( "time" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -14,9 +15,8 @@ var _ = Describe("Upstreams", func() { errStrings []string } - flushInterval := options.Duration(5 * time.Second) + flushInterval := 5 * time.Second staticCode200 := 200 - truth := true validHTTPUpstream := options.Upstream{ ID: "validHTTPUpstream", @@ -26,7 +26,7 @@ var _ = Describe("Upstreams", func() { validStaticUpstream := options.Upstream{ ID: "validStaticUpstream", Path: "/validStaticUpstream", - Static: true, + Static: ptr.Ptr(true), } validFileUpstream := options.Upstream{ ID: "validFileUpstream", @@ -145,11 +145,11 @@ var _ = Describe("Upstreams", func() { ID: "foo", Path: "/foo", URI: "ftp://foo", - Static: true, + Static: ptr.Ptr(true), FlushInterval: &flushInterval, - PassHostHeader: &truth, - ProxyWebSockets: &truth, - InsecureSkipTLSVerify: true, + PassHostHeader: ptr.Ptr(true), + ProxyWebSockets: ptr.Ptr(true), + InsecureSkipTLSVerify: ptr.Ptr(true), }, }, }, diff --git a/providers/adfs.go b/providers/adfs.go index 0facfdcf..6615f38c 100644 --- a/providers/adfs.go +++ b/providers/adfs.go @@ -50,7 +50,7 @@ func NewADFSProvider(p *ProviderData, opts options.Provider) *ADFSProvider { return &ADFSProvider{ OIDCProvider: oidcProvider, - skipScope: opts.ADFSConfig.SkipScope, + skipScope: *opts.ADFSConfig.SkipScope, oidcEnrichFunc: oidcProvider.EnrichSession, oidcRefreshFunc: oidcProvider.RefreshSession, } diff --git a/providers/adfs_test.go b/providers/adfs_test.go index 0b730430..edcb9307 100755 --- a/providers/adfs_test.go +++ b/providers/adfs_test.go @@ -16,6 +16,7 @@ import ( "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -172,7 +173,7 @@ var _ = Describe("ADFS Provider Tests", func() { ProtectedResource: resource, Scope: "", }, options.Provider{ - ADFSConfig: options.ADFSOptions{SkipScope: true}, + ADFSConfig: options.ADFSOptions{SkipScope: ptr.Ptr(true)}, }) result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "", url.Values{}) diff --git a/providers/google.go b/providers/google.go index 097e3567..7926c71b 100644 --- a/providers/google.go +++ b/providers/google.go @@ -102,7 +102,7 @@ func NewGoogleProvider(p *ProviderData, opts options.GoogleOptions) (*GoogleProv }, } - if opts.ServiceAccountJSON != "" || opts.UseApplicationDefaultCredentials { + if opts.ServiceAccountJSON != "" || *opts.UseApplicationDefaultCredentials { provider.configureGroups(opts) } @@ -259,7 +259,7 @@ var possibleScopesList = [...]string{ } func getOauth2TokenSource(ctx context.Context, opts options.GoogleOptions, scope string) oauth2.TokenSource { - if opts.UseApplicationDefaultCredentials { + if *opts.UseApplicationDefaultCredentials { ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ TargetPrincipal: getTargetPrincipal(ctx, opts), Scopes: []string{scope}, diff --git a/providers/ms_entra_id.go b/providers/ms_entra_id.go index df1f38a4..752f9f44 100644 --- a/providers/ms_entra_id.go +++ b/providers/ms_entra_id.go @@ -51,7 +51,7 @@ func NewMicrosoftEntraIDProvider(p *ProviderData, opts options.Provider) *Micros OIDCProvider: NewOIDCProvider(p, opts.OIDCConfig), multiTenantAllowedTenants: opts.MicrosoftEntraIDConfig.AllowedTenants, - federatedTokenAuth: opts.MicrosoftEntraIDConfig.FederatedTokenAuth, + federatedTokenAuth: *opts.MicrosoftEntraIDConfig.FederatedTokenAuth, microsoftGraphURL: microsoftGraphURL, } } diff --git a/providers/ms_entra_id_test.go b/providers/ms_entra_id_test.go index dfd1ef99..7b720c91 100644 --- a/providers/ms_entra_id_test.go +++ b/providers/ms_entra_id_test.go @@ -13,6 +13,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt/v5" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" "github.com/stretchr/testify/assert" . "github.com/onsi/gomega" @@ -24,7 +25,7 @@ func TestAzureEntraOIDCProviderNewMultiTenant(t *testing.T) { provider := NewMicrosoftEntraIDProvider(&ProviderData{}, options.Provider{OIDCConfig: options.OIDCOptions{ IssuerURL: "https://login.microsoftonline.com/common/v2.0", - InsecureSkipIssuerVerification: true, + InsecureSkipIssuerVerification: ptr.Ptr(true), }}, ) g.Expect(provider.Data().ProviderName).To(Equal("Microsoft Entra ID")) @@ -90,8 +91,8 @@ func TestAzureEntraOIDCProviderValidateSessionAllowedTenants(t *testing.T) { options.Provider{ OIDCConfig: options.OIDCOptions{ IssuerURL: "https://login.microsoftonline.com/common/v2.0", - InsecureSkipIssuerVerification: true, - InsecureSkipNonce: true, + InsecureSkipIssuerVerification: ptr.Ptr(true), + InsecureSkipNonce: ptr.Ptr(true), }, MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{ AllowedTenants: []string{"85d7d600-7804-4d92-8d43-9c33c21c130c"}, diff --git a/providers/oidc.go b/providers/oidc.go index 15598aba..9a88b46c 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -50,7 +50,7 @@ func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider { return &OIDCProvider{ ProviderData: p, - SkipNonce: opts.InsecureSkipNonce, + SkipNonce: *opts.InsecureSkipNonce, } } diff --git a/providers/oidc_test.go b/providers/oidc_test.go index 81a70eb4..61f4762e 100644 --- a/providers/oidc_test.go +++ b/providers/oidc_test.go @@ -63,7 +63,7 @@ func newOIDCProvider(serverURL *url.URL, skipNonce bool) *OIDCProvider { } p := NewOIDCProvider(providerData, options.OIDCOptions{ - InsecureSkipNonce: skipNonce, + InsecureSkipNonce: &skipNonce, }) return p diff --git a/providers/providers.go b/providers/providers.go index ec00f412..84f5ec91 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -98,8 +98,8 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, IssuerURL: providerConfig.OIDCConfig.IssuerURL, JWKsURL: providerConfig.OIDCConfig.JwksURL, PublicKeyFiles: providerConfig.OIDCConfig.PublicKeyFiles, - SkipDiscovery: providerConfig.OIDCConfig.SkipDiscovery, - SkipIssuerVerification: providerConfig.OIDCConfig.InsecureSkipIssuerVerification, + SkipDiscovery: *providerConfig.OIDCConfig.SkipDiscovery, + SkipIssuerVerification: *providerConfig.OIDCConfig.InsecureSkipIssuerVerification, }) if err != nil { return nil, fmt.Errorf("error building OIDC ProviderVerifier: %v", err) @@ -143,10 +143,10 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData, } // Make the OIDC options available to all providers that support it - p.AllowUnverifiedEmail = providerConfig.OIDCConfig.InsecureAllowUnverifiedEmail + p.AllowUnverifiedEmail = *providerConfig.OIDCConfig.InsecureAllowUnverifiedEmail p.EmailClaim = providerConfig.OIDCConfig.EmailClaim p.GroupsClaim = providerConfig.OIDCConfig.GroupsClaim - p.SkipClaimsFromProfileURL = providerConfig.SkipClaimsFromProfileURL + p.SkipClaimsFromProfileURL = *providerConfig.SkipClaimsFromProfileURL // Set PKCE enabled or disabled based on discovery and force options p.CodeChallengeMethod = parseCodeChallengeMethod(providerConfig) diff --git a/providers/providers_test.go b/providers/providers_test.go index 82961d84..9591dc65 100644 --- a/providers/providers_test.go +++ b/providers/providers_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" . "github.com/onsi/gomega" ) @@ -81,7 +82,7 @@ func TestSkipOIDCDiscovery(t *testing.T) { ClientSecretFile: clientSecret, OIDCConfig: options.OIDCOptions{ IssuerURL: msIssuerURL, - SkipDiscovery: true, + SkipDiscovery: ptr.Ptr(true), }, } @@ -108,7 +109,7 @@ func TestURLsCorrectlyParsed(t *testing.T) { RedeemURL: msTokenURL, OIDCConfig: options.OIDCOptions{ IssuerURL: msIssuerURL, - SkipDiscovery: true, + SkipDiscovery: ptr.Ptr(true), JwksURL: msKeysURL, }, } @@ -216,7 +217,7 @@ func TestScope(t *testing.T) { AllowedGroups: tc.allowedGroups, OIDCConfig: options.OIDCOptions{ IssuerURL: msIssuerURL, - SkipDiscovery: true, + SkipDiscovery: ptr.Ptr(true), JwksURL: msKeysURL, }, } @@ -297,7 +298,7 @@ func TestEmailClaimCorrectlySet(t *testing.T) { RedeemURL: msTokenURL, OIDCConfig: options.OIDCOptions{ IssuerURL: msIssuerURL, - SkipDiscovery: true, + SkipDiscovery: ptr.Ptr(true), JwksURL: msKeysURL, UserIDClaim: tc.userIDClaim, EmailClaim: tc.emailClaim,