From e64322b0701b6722a6f873042b735dfa50497cd5 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Sat, 12 Sep 2020 13:47:42 +0900 Subject: [PATCH 01/17] Update Go to 1.15 --- .travis.yml | 2 +- CHANGELOG.md | 1 + Dockerfile | 2 +- Dockerfile.arm64 | 2 +- Dockerfile.armv6 | 2 +- go.mod | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b39182a..60e56015 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - 1.14.x + - 1.15.x env: - COVER=true install: diff --git a/CHANGELOG.md b/CHANGELOG.md index e49f3e64..e25171c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - [#764](https://github.com/oauth2-proxy/oauth2-proxy/pull/764) Document bcrypt encryption for htpasswd (and hide SHA) (@lentzi90) - [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Add support to ensure user belongs in required groups when using the OIDC provider (@stefansedich) - [#800](https://github.com/oauth2-proxy/oauth2-proxy/pull/800) Fix import path for v7 (@johejo) +- [#783](https://github.com/oauth2-proxy/oauth2-proxy/pull/783) Update Go to 1.15 (@johejo) # v6.1.1 diff --git a/Dockerfile b/Dockerfile index a989cb73..310eec1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.14-buster AS builder +FROM golang:1.15-buster AS builder ARG VERSION # Download tools diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 642770fe..4f7433f9 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,4 +1,4 @@ -FROM golang:1.14-buster AS builder +FROM golang:1.15-buster AS builder ARG VERSION # Download tools diff --git a/Dockerfile.armv6 b/Dockerfile.armv6 index 2d16e3e2..a69cf318 100644 --- a/Dockerfile.armv6 +++ b/Dockerfile.armv6 @@ -1,4 +1,4 @@ -FROM golang:1.14-buster AS builder +FROM golang:1.15-buster AS builder ARG VERSION # Download tools diff --git a/go.mod b/go.mod index 7e4d28bf..2b3ec136 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/oauth2-proxy/oauth2-proxy/v7 -go 1.14 +go 1.15 require ( github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb From 8be97f25e7276d2d46f002199f9eda345fc0800d Mon Sep 17 00:00:00 2001 From: Thiago Caiubi Date: Sat, 3 Oct 2020 10:09:40 -0300 Subject: [PATCH 02/17] Fix build (#813) * Fix build Without the v7 path it builds old version of the project (v3.2.0). * Update CHANGELOG.md --- CHANGELOG.md | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e25171c2..6e7824f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Add support to ensure user belongs in required groups when using the OIDC provider (@stefansedich) - [#800](https://github.com/oauth2-proxy/oauth2-proxy/pull/800) Fix import path for v7 (@johejo) - [#783](https://github.com/oauth2-proxy/oauth2-proxy/pull/783) Update Go to 1.15 (@johejo) +- [#813](https://github.com/oauth2-proxy/oauth2-proxy/pull/813) Fix build (@thiagocaiubi) # v6.1.1 diff --git a/Makefile b/Makefile index 19b91a18..3b7ea3b4 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ lint: validate-go-version build: validate-go-version clean $(BINARY) $(BINARY): - GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/oauth2-proxy/oauth2-proxy + GO111MODULE=on CGO_ENABLED=0 $(GO) build -a -installsuffix cgo -ldflags="-X main.VERSION=${VERSION}" -o $@ github.com/oauth2-proxy/oauth2-proxy/v7 .PHONY: docker docker: From dc7dbc5d286be384cca118fe7c686c54f1759515 Mon Sep 17 00:00:00 2001 From: Shinebayar G <3091558+shinebayar-g@users.noreply.github.com> Date: Mon, 5 Oct 2020 04:29:47 -0500 Subject: [PATCH 03/17] ci: migrate to Github Actions, close #546 (#750) * ci: migrate to Github Actions * ci: optimize on feedback * ci: run gocov in correct dir * ci: running after-build script always * ci: giving test script execute permission * ci: correct error handling on test script * ci: more verbose test script * ci: configure CC_TEST_REPORTER_ID env * ci: check existence of CC_TEST_REPORT_ID variable, skip if unset * ci: check existence of CC_TEST_REPORT_ID variable, skip if unset * update changelog * Update CHANGELOG.md Co-authored-by: Joel Speed --- .github/workflows/ci.yaml | 55 +++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + test.sh | 27 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100755 test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..c5e63403 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,55 @@ +name: Continuous Integration + +on: + push: + branches: + - '**' + # - $default-branch + pull_request: + branches: + - '**' + # - $default-branch + +jobs: + build: + env: + COVER: true + GOPATH: ${{ github.workspace }} + runs-on: ubuntu-18.04 + steps: + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + with: + path: ./src/github.com/${{ github.repository }} + + - name: Set up Go 1.14 + uses: actions/setup-go@v2 + with: + go-version: ^1.14 + id: go + + - name: Get dependencies + run: | + cd src/github.com/${{ github.repository }} + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.24.0 + GO111MODULE=on go mod download + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + + - name: Lint + run: | + cd src/github.com/${{ github.repository }} + make lint + + - name: Build + run: | + cd src/github.com/${{ github.repository }} + make build + + - name: Test + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + run: | + cd src/github.com/${{ github.repository }} + ./test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e7824f0..41dabfa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - [#800](https://github.com/oauth2-proxy/oauth2-proxy/pull/800) Fix import path for v7 (@johejo) - [#783](https://github.com/oauth2-proxy/oauth2-proxy/pull/783) Update Go to 1.15 (@johejo) - [#813](https://github.com/oauth2-proxy/oauth2-proxy/pull/813) Fix build (@thiagocaiubi) +- [#750](https://github.com/oauth2-proxy/oauth2-proxy/pull/750) ci: Migrate to Github Actions (@shinebayar-g) # v6.1.1 diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..c4f6ef85 --- /dev/null +++ b/test.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# manually exiting from script, because after-build needs to run always +set +e + +if [ -z $CC_TEST_REPORT_ID ]; then + echo "1. CC_TEST_REPORT_ID is unset, skipping" +else + echo "1. Running before-build" + ./cc-test-reporter before-build +fi + +echo "2. Running test" +make test +TEST_STATUS=$? +echo "TEST_STATUS: ${TEST_STATUS}" + +if [ -z $CC_TEST_REPORT_ID ]; then + echo "3. CC_TEST_REPORT_ID is unset, skipping" +else + echo "3. Running after-build" + ./cc-test-reporter after-build --exit-code $TEST_STATUS -t gocov +fi + +if [ "$TEST_STATUS" -ne 0 ]; then + echo "Test failed, status code: $TEST_STATUS" + exit $TEST_STATUS +fi From 3d203a1a03834a9b0c80c96b09983a552c8483a1 Mon Sep 17 00:00:00 2001 From: Jakub Holy Date: Mon, 5 Oct 2020 11:34:42 +0200 Subject: [PATCH 04/17] Home: Add a brief description of the behavior (#794) * Home: Add a brief description of the behavior I could not find this information anywhere and think it is quite important for understanding how to use and configure the proxy for different use cases. (Especially the Ajax part is not mentioned anywhere else I believe.) I tried to keep it general enough so that it won't need updating often yet useful enough to have good value :) * Update docs/0_index.md Co-authored-by: Joel Speed Co-authored-by: Joel Speed --- docs/0_index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/0_index.md b/docs/0_index.md index e724e0f8..860ba372 100644 --- a/docs/0_index.md +++ b/docs/0_index.md @@ -21,3 +21,12 @@ A list of changes can be seen in the [CHANGELOG]({{ site.gitweb }}/CHANGELOG.md) ## Architecture ![OAuth2 Proxy Architecture](https://cloud.githubusercontent.com/assets/45028/8027702/bd040b7a-0d6a-11e5-85b9-f8d953d04f39.png) + +## Behavior + +1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). +2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) +3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set +4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) + +Notice that the proxy also provides a number of useful [endpoints](/oauth2-proxy/endpoints). From 5c62690653ee743f19beabb8cc86af2a9179ff31 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Tue, 6 Oct 2020 21:34:32 +0900 Subject: [PATCH 05/17] Rename test directory to testdata See https://golang.org/cmd/go/#hdr-Test_packages --- CHANGELOG.md | 1 + oauthproxy_test.go | 2 +- {test => testdata}/openredirects.txt | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename {test => testdata}/openredirects.txt (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41dabfa9..be824094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - [#783](https://github.com/oauth2-proxy/oauth2-proxy/pull/783) Update Go to 1.15 (@johejo) - [#813](https://github.com/oauth2-proxy/oauth2-proxy/pull/813) Fix build (@thiagocaiubi) - [#750](https://github.com/oauth2-proxy/oauth2-proxy/pull/750) ci: Migrate to Github Actions (@shinebayar-g) +- [#829](https://github.com/oauth2-proxy/oauth2-proxy/pull/820) Rename test directory to testdata (@johejo) # v6.1.1 diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 33f131cd..d0feec2d 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -330,7 +330,7 @@ func TestOpenRedirects(t *testing.T) { t.Fatal(err) } - file, err := os.Open("./test/openredirects.txt") + file, err := os.Open("./testdata/openredirects.txt") if err != nil { t.Fatal(err) } diff --git a/test/openredirects.txt b/testdata/openredirects.txt similarity index 100% rename from test/openredirects.txt rename to testdata/openredirects.txt From fcb83c48f4902bdc67924ac3b88c8af82847d54e Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 7 Oct 2020 19:49:27 +0900 Subject: [PATCH 06/17] Update go-redis/redis to v8 (#801) * update go-redis/redis to v8 testify, ginko and gomega have also been updated. * update changelog * Update pkg/sessions/redis/redis_store_test.go Co-authored-by: Joel Speed Co-authored-by: Joel Speed --- CHANGELOG.md | 1 + go.mod | 8 ++-- go.sum | 54 ++++++++++---------------- pkg/sessions/redis/client.go | 14 +++---- pkg/sessions/redis/redis_store.go | 2 +- pkg/sessions/redis/redis_store_test.go | 15 ++++++- pkg/validation/sessions_test.go | 4 +- 7 files changed, 48 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be824094..eda96f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - [#800](https://github.com/oauth2-proxy/oauth2-proxy/pull/800) Fix import path for v7 (@johejo) - [#783](https://github.com/oauth2-proxy/oauth2-proxy/pull/783) Update Go to 1.15 (@johejo) - [#813](https://github.com/oauth2-proxy/oauth2-proxy/pull/813) Fix build (@thiagocaiubi) +- [#801](https://github.com/oauth2-proxy/oauth2-proxy/pull/801) Update go-redis/redis to v8 (@johejo) - [#750](https://github.com/oauth2-proxy/oauth2-proxy/pull/750) ci: Migrate to Github Actions (@shinebayar-g) - [#829](https://github.com/oauth2-proxy/oauth2-proxy/pull/820) Rename test directory to testdata (@johejo) diff --git a/go.mod b/go.mod index 2b3ec136..fcacdcc1 100644 --- a/go.mod +++ b/go.mod @@ -11,17 +11,17 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/frankban/quicktest v1.10.0 // indirect github.com/fsnotify/fsnotify v1.4.9 - github.com/go-redis/redis/v7 v7.2.0 + github.com/go-redis/redis/v8 v8.2.3 github.com/justinas/alice v1.2.0 github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa github.com/mitchellh/mapstructure v1.1.2 - github.com/onsi/ginkgo v1.14.0 - github.com/onsi/gomega v1.10.1 + github.com/onsi/ginkgo v1.14.1 + github.com/onsi/gomega v1.10.2 github.com/pierrec/lz4 v2.5.2+incompatible github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.6.3 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.6.1 github.com/vmihailenco/msgpack/v4 v4.3.11 github.com/yhat/wsutil v0.0.0-20170731153501-1d66fa95c997 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 diff --git a/go.sum b/go.sum index 4f4922cf..08c16307 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,6 @@ github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2F github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= @@ -26,7 +25,10 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -38,18 +40,18 @@ github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHo github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -57,8 +59,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs= -github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.2.3 h1:eNesND+DWt/sjQOtPFxAbQkTIXaXX00qNLxjVWkZ70k= +github.com/go-redis/redis/v8 v8.2.3/go.mod h1:ysgGY09J/QeDYbu3HikWEIPCwaeOkuNoTgKayTEaEOw= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -69,9 +71,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -80,18 +80,17 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -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/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8= github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -111,7 +110,6 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -122,7 +120,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V 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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -149,14 +146,13 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= @@ -198,8 +194,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -218,6 +215,8 @@ github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBU go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -238,19 +237,14 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -266,12 +260,9 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= @@ -289,7 +280,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -298,7 +288,6 @@ google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -320,12 +309,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -338,12 +324,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/sessions/redis/client.go b/pkg/sessions/redis/client.go index 6037ce47..3d312b34 100644 --- a/pkg/sessions/redis/client.go +++ b/pkg/sessions/redis/client.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/go-redis/redis/v7" + "github.com/go-redis/redis/v8" ) // Client is wrapper interface for redis.Client and redis.ClusterClient. @@ -25,15 +25,15 @@ func newClient(c *redis.Client) Client { } func (c *client) Get(ctx context.Context, key string) ([]byte, error) { - return c.WithContext(ctx).Get(key).Bytes() + return c.Client.Get(ctx, key).Bytes() } func (c *client) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error { - return c.WithContext(ctx).Set(key, value, expiration).Err() + return c.Client.Set(ctx, key, value, expiration).Err() } func (c *client) Del(ctx context.Context, key string) error { - return c.WithContext(ctx).Del(key).Err() + return c.Client.Del(ctx, key).Err() } var _ Client = (*clusterClient)(nil) @@ -47,13 +47,13 @@ func newClusterClient(c *redis.ClusterClient) Client { } func (c *clusterClient) Get(ctx context.Context, key string) ([]byte, error) { - return c.WithContext(ctx).Get(key).Bytes() + return c.ClusterClient.Get(ctx, key).Bytes() } func (c *clusterClient) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error { - return c.WithContext(ctx).Set(key, value, expiration).Err() + return c.ClusterClient.Set(ctx, key, value, expiration).Err() } func (c *clusterClient) Del(ctx context.Context, key string) error { - return c.WithContext(ctx).Del(key).Err() + return c.ClusterClient.Del(ctx, key).Err() } diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index ebd9ad19..5de5ce5a 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -7,7 +7,7 @@ import ( "io/ioutil" "time" - "github.com/go-redis/redis/v7" + "github.com/go-redis/redis/v8" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" diff --git a/pkg/sessions/redis/redis_store_test.go b/pkg/sessions/redis/redis_store_test.go index d34d007c..2711f85c 100644 --- a/pkg/sessions/redis/redis_store_test.go +++ b/pkg/sessions/redis/redis_store_test.go @@ -1,6 +1,7 @@ package redis import ( + "context" "log" "os" "testing" @@ -8,7 +9,7 @@ import ( "github.com/Bose/minisentinel" "github.com/alicebob/miniredis/v2" - "github.com/go-redis/redis/v7" + "github.com/go-redis/redis/v8" "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/logger" @@ -20,10 +21,20 @@ import ( const redisPassword = "0123456789abcdefghijklmnopqrstuv" +// wrappedRedisLogger wraps a logger so that we can coerce the logger to +// fit the expected signature for go-redis logging +type wrappedRedisLogger struct { + *log.Logger +} + +func (l *wrappedRedisLogger) Printf(_ context.Context, format string, v ...interface{}) { + l.Logger.Printf(format, v...) +} + func TestSessionStore(t *testing.T) { logger.SetOutput(GinkgoWriter) - redisLogger := log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile) + redisLogger := &wrappedRedisLogger{Logger: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile)} redisLogger.SetOutput(GinkgoWriter) redis.SetLogger(redisLogger) diff --git a/pkg/validation/sessions_test.go b/pkg/validation/sessions_test.go index 68fac8e1..f6463431 100644 --- a/pkg/validation/sessions_test.go +++ b/pkg/validation/sessions_test.go @@ -121,8 +121,8 @@ var _ = Describe("Sessions", func() { const ( clusterAndSentinelMsg = "unable to initialize a redis client: options redis-use-sentinel and redis-use-cluster are mutually exclusive" - parseWrongSchemeMsg = "unable to initialize a redis client: unable to parse redis url: invalid redis URL scheme: https" - parseWrongFormatMsg = "unable to initialize a redis client: unable to parse redis url: invalid redis database number: \"wrong\"" + parseWrongSchemeMsg = "unable to initialize a redis client: unable to parse redis url: redis: invalid URL scheme: https" + parseWrongFormatMsg = "unable to initialize a redis client: unable to parse redis url: redis: invalid database number: \"wrong\"" invalidPasswordSetMsg = "unable to set a redis initialization key: WRONGPASS invalid username-password pair" invalidPasswordDelMsg = "unable to delete the redis initialization key: WRONGPASS invalid username-password pair" unreachableRedisSetMsg = "unable to set a redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused" From 183cb124a4a2e825f44865aef7c18a29f9536796 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Tue, 22 Sep 2020 18:54:32 -0700 Subject: [PATCH 07/17] Support HTTP method based allowlists --- CHANGELOG.md | 4 + docs/configuration/configuration.md | 5 +- oauthproxy.go | 86 ++++++++++++---- pkg/apis/options/options.go | 10 +- pkg/validation/allowlist.go | 70 +++++++++++++ pkg/validation/allowlist_test.go | 149 ++++++++++++++++++++++++++++ pkg/validation/options.go | 24 +---- pkg/validation/options_test.go | 86 ++-------------- 8 files changed, 309 insertions(+), 125 deletions(-) create mode 100644 pkg/validation/allowlist.go create mode 100644 pkg/validation/allowlist_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index eda96f1a..c9019068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Sessions from v5.1.1 or earlier will no longer validate since they were not signed with SHA1. - Sessions from v6.0.0 or later had a graceful conversion to SHA256 that resulted in no reauthentication - Upgrading from v5.1.1 or earlier will result in a reauthentication +- [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex` + - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version. + - If your regex contains an `=` and you want it for all methods, you will need to add a leading `=` (this is the area where `--skip-auth-regex` doesn't port perfectly) - [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Ensure you have configured oauth2-proxy to use the `groups` scope. The user may be logged out initially as they may not currently have the `groups` claim however after going back through login process wil be authenticated. ## Breaking Changes @@ -23,6 +26,7 @@ ## Changes since v6.1.1 - [#753](https://github.com/oauth2-proxy/oauth2-proxy/pull/753) Pass resource parameter in login url (@codablock) +- [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) Add `--skip-auth-route` configuration option for `METHOD=pathRegex` based allowlists (@NickMeves) - [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Stop accepting legacy SHA1 signed cookies (@NickMeves) - [#722](https://github.com/oauth2-proxy/oauth2-proxy/pull/722) Validate Redis configuration options at startup (@NickMeves) - [#791](https://github.com/oauth2-proxy/oauth2-proxy/pull/791) Remove GetPreferredUsername method from provider interface (@NickMeves) diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 0370cdf4..8fbc406d 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -119,8 +119,9 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example | `--signature-key` | string | GAP-Signature request signature key (algorithm:secretkey) | | | `--silence-ping-logging` | bool | disable logging of requests to ping endpoint | false | | `--skip-auth-preflight` | bool | will skip authentication for OPTIONS requests | false | -| `--skip-auth-regex` | string | bypass authentication for requests paths that match (may be given multiple times) | | -| `--skip-auth-strip-headers` | bool | strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy for request paths in `--skip-auth-regex` | false | +| `--skip-auth-regex` | string \| list | (DEPRECATED for `--skip-auth-route`) bypass authentication for requests paths that match (may be given multiple times) | | +| `--skip-auth-route` | string \| list | bypass authentication for requests that match the method & path. Format: method=path_regex OR path_regex alone for all methods | | +| `--skip-auth-strip-headers` | bool | strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy for allowlisted requests (`--skip-auth-route`, `--skip-auth-regex`, `--skip-auth-preflight`, `--trusted-ip`) | false | | `--skip-jwt-bearer-tokens` | bool | will skip requests that have verified JWT bearer tokens | false | | `--skip-oidc-discovery` | bool | bypass OIDC endpoint discovery. `--login-url`, `--redeem-url` and `--oidc-jwks-url` must be configured in this case | false | | `--skip-provider-button` | bool | will skip sign-in-page to directly reach the next step: oauth/start | false | diff --git a/oauthproxy.go b/oauthproxy.go index 092dcc93..ddeafdcd 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -48,6 +48,12 @@ var ( invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`) ) +// allowedRoute manages method + path based allowlists +type allowedRoute struct { + method string + pathRegex *regexp.Regexp +} + // OAuthProxy is the main authentication proxy type OAuthProxy struct { CookieSeed string @@ -70,6 +76,7 @@ type OAuthProxy struct { AuthOnlyPath string UserInfoPath string + allowedRoutes []*allowedRoute redirectURL *url.URL // the url to receive requests at whitelistDomains []string provider providers.Provider @@ -90,13 +97,11 @@ type OAuthProxy struct { SetAuthorization bool PassAuthorization bool PreferEmailToUser bool - skipAuthRegex []string skipAuthPreflight bool skipAuthStripHeaders bool skipJwtBearerTokens bool mainJwtBearerVerifier *oidc.IDTokenVerifier extraJwtBearerVerifiers []*oidc.IDTokenVerifier - compiledRegex []*regexp.Regexp templates *template.Template realClientIPParser ipapi.RealClientIPParser trustedIPs *ip.NetSet @@ -121,10 +126,6 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr return nil, fmt.Errorf("error initialising upstream proxy: %v", err) } - for _, u := range opts.GetCompiledRegex() { - logger.Printf("compiled skip-auth-regex => %q", u) - } - if opts.SkipJwtBearerTokens { logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL) for _, issuer := range opts.ExtraJwtIssuers { @@ -163,6 +164,11 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr } } + allowedRoutes, err := buildRoutesAllowlist(opts) + if err != nil { + return nil, err + } + sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator) return &OAuthProxy{ @@ -192,14 +198,13 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr sessionStore: sessionStore, serveMux: upstreamProxy, redirectURL: redirectURL, + allowedRoutes: allowedRoutes, whitelistDomains: opts.WhitelistDomains, - skipAuthRegex: opts.SkipAuthRegex, skipAuthPreflight: opts.SkipAuthPreflight, skipAuthStripHeaders: opts.SkipAuthStripHeaders, skipJwtBearerTokens: opts.SkipJwtBearerTokens, mainJwtBearerVerifier: opts.GetOIDCVerifier(), extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(), - compiledRegex: opts.GetCompiledRegex(), realClientIPParser: opts.GetRealClientIPParser(), SetXAuthRequest: opts.SetXAuthRequest, PassBasicAuth: opts.PassBasicAuth, @@ -277,6 +282,51 @@ func buildSignInMessage(opts *options.Options) string { return msg } +// buildRoutesAllowlist builds an []allowedRoute list from either the legacy +// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option +// (method=path support) +func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { + var routes []*allowedRoute + + for _, path := range opts.SkipAuthRegex { + compiledRegex, err := regexp.Compile(path) + if err != nil { + return nil, err + } + routes = append(routes, &allowedRoute{ + method: "", + pathRegex: compiledRegex, + }) + } + + for _, methodPath := range opts.SkipAuthRoutes { + var ( + method string + path string + ) + + parts := strings.Split(methodPath, "=") + if len(parts) == 1 { + method = "" + path = parts[0] + } else { + method = strings.ToUpper(parts[0]) + path = strings.Join(parts[1:], "=") + } + + compiledRegex, err := regexp.Compile(path) + if err != nil { + return nil, err + } + routes = append(routes, &allowedRoute{ + method: method, + pathRegex: compiledRegex, + }) + } + + return routes, nil +} + // GetRedirectURI returns the redirectURL that the upstream OAuth Provider will // redirect clients to once authenticated func (p *OAuthProxy) GetRedirectURI(host string) string { @@ -584,16 +634,16 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool { } } -// IsWhitelistedRequest is used to check if auth should be skipped for this request -func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool { +// IsAllowedRequest is used to check if auth should be skipped for this request +func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool { isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" - return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req) + return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.IsTrustedIP(req) } -// IsWhitelistedPath is used to check if the request path is allowed without auth -func (p *OAuthProxy) IsWhitelistedPath(path string) bool { - for _, u := range p.compiledRegex { - if u.MatchString(path) { +// IsAllowedRoute is used to check if the request method & path is allowed without auth +func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool { + for _, route := range p.allowedRoutes { + if (route.method == "" || req.Method == route.method) && route.pathRegex.MatchString(req.URL.Path) { return true } } @@ -643,7 +693,7 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { switch path := req.URL.Path; { case path == p.RobotsPath: p.RobotsTxt(rw) - case p.IsWhitelistedRequest(req): + case p.IsAllowedRequest(req): p.SkipAuthProxy(rw, req) case path == p.SignInPath: p.SignIn(rw, req) @@ -831,7 +881,7 @@ func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) rw.WriteHeader(http.StatusAccepted) } -// SkipAuthProxy proxies whitelisted requests and skips authentication +// SkipAuthProxy proxies allowlisted requests and skips authentication func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) { if p.skipAuthStripHeaders { p.stripAuthHeaders(req) @@ -1026,7 +1076,7 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req } } -// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex +// stripAuthHeaders removes Auth headers for allowlisted routes from skipAuthRegex func (p *OAuthProxy) stripAuthHeaders(req *http.Request) { if p.PassBasicAuth { req.Header.Del("X-Forwarded-User") diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index bcb600e9..a79d1520 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -3,7 +3,6 @@ package options import ( "crypto" "net/url" - "regexp" oidc "github.com/coreos/go-oidc" ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip" @@ -67,6 +66,7 @@ type Options struct { UpstreamServers Upstreams `cfg:",internal"` SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` + SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"` SkipAuthStripHeaders bool `flag:"skip-auth-strip-headers" cfg:"skip_auth_strip_headers"` SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"` ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"` @@ -114,7 +114,6 @@ type Options struct { // internal values that are set after config validation redirectURL *url.URL - compiledRegex []*regexp.Regexp provider providers.Provider signatureData *SignatureData oidcVerifier *oidc.IDTokenVerifier @@ -124,7 +123,6 @@ type Options struct { // Options for Getting internal values func (o *Options) GetRedirectURL() *url.URL { return o.redirectURL } -func (o *Options) GetCompiledRegex() []*regexp.Regexp { return o.compiledRegex } func (o *Options) GetProvider() providers.Provider { return o.provider } func (o *Options) GetSignatureData() *SignatureData { return o.signatureData } func (o *Options) GetOIDCVerifier() *oidc.IDTokenVerifier { return o.oidcVerifier } @@ -133,7 +131,6 @@ func (o *Options) GetRealClientIPParser() ipapi.RealClientIPParser { return o.re // Options for Setting internal values func (o *Options) SetRedirectURL(s *url.URL) { o.redirectURL = s } -func (o *Options) SetCompiledRegex(s []*regexp.Regexp) { o.compiledRegex = s } func (o *Options) SetProvider(s providers.Provider) { o.provider = s } func (o *Options) SetSignatureData(s *SignatureData) { o.signatureData = s } func (o *Options) SetOIDCVerifier(s *oidc.IDTokenVerifier) { o.oidcVerifier = s } @@ -195,8 +192,9 @@ func NewFlagSet() *pflag.FlagSet { flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header") flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream") flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)") - flagSet.StringSlice("skip-auth-regex", []string{}, "bypass authentication for requests path's that match (may be given multiple times)") - flagSet.Bool("skip-auth-strip-headers", false, "strips X-Forwarded-* style authentication headers & Authorization header if they would be set by oauth2-proxy for request paths in --skip-auth-regex") + flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)") + flagSet.StringSlice("skip-auth-route", []string{}, "bypass authentication for requests that match the method & path. Format: method=path_regex OR path_regex alone for all methods") + flagSet.Bool("skip-auth-strip-headers", false, "strips `X-Forwarded-*` style authentication headers & `Authorization` header if they would be set by oauth2-proxy for allowlisted requests (`--skip-auth-route`, `--skip-auth-regex`, `--skip-auth-preflight`, `--trusted-ip`)") flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start") flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests") flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers") diff --git a/pkg/validation/allowlist.go b/pkg/validation/allowlist.go new file mode 100644 index 00000000..254983d8 --- /dev/null +++ b/pkg/validation/allowlist.go @@ -0,0 +1,70 @@ +package validation + +import ( + "fmt" + "os" + "regexp" + "strings" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/pkg/ip" +) + +func validateAllowlists(o *options.Options) []string { + msgs := []string{} + + msgs = append(msgs, validateRoutes(o)...) + msgs = append(msgs, validateRegexes(o)...) + msgs = append(msgs, validateTrustedIPs(o)...) + + if len(o.TrustedIPs) > 0 && o.ReverseProxy { + _, err := fmt.Fprintln(os.Stderr, "WARNING: mixing --trusted-ip with --reverse-proxy is a potential security vulnerability. An attacker can inject a trusted IP into an X-Real-IP or X-Forwarded-For header if they aren't properly protected outside of oauth2-proxy") + if err != nil { + panic(err) + } + } + + return msgs +} + +// validateRoutes validates method=path routes passed with options.SkipAuthRoutes +func validateRoutes(o *options.Options) []string { + msgs := []string{} + for _, route := range o.SkipAuthRoutes { + var regex string + parts := strings.Split(route, "=") + if len(parts) == 1 { + regex = parts[0] + } else { + regex = strings.Join(parts[1:], "=") + } + _, err := regexp.Compile(regex) + if err != nil { + msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err)) + } + } + return msgs +} + +// validateRegex validates regex paths passed with options.SkipAuthRegex +func validateRegexes(o *options.Options) []string { + msgs := []string{} + for _, regex := range o.SkipAuthRegex { + _, err := regexp.Compile(regex) + if err != nil { + msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err)) + } + } + return msgs +} + +// validateTrustedIPs validates IP/CIDRs for IP based allowlists +func validateTrustedIPs(o *options.Options) []string { + msgs := []string{} + for i, ipStr := range o.TrustedIPs { + if nil == ip.ParseIPNet(ipStr) { + msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr)) + } + } + return msgs +} diff --git a/pkg/validation/allowlist_test.go b/pkg/validation/allowlist_test.go new file mode 100644 index 00000000..a17bb12d --- /dev/null +++ b/pkg/validation/allowlist_test.go @@ -0,0 +1,149 @@ +package validation + +import ( + "testing" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" + "github.com/stretchr/testify/assert" +) + +func Test_validateAllowlists(t *testing.T) { + opts := &options.Options{ + SkipAuthRoutes: []string{ + "POST=/foo/bar", + "PUT=^/foo/bar$", + }, + SkipAuthRegex: []string{"/foo/baz"}, + TrustedIPs: []string{ + "10.32.0.1/32", + "43.36.201.0/24", + }, + } + assert.Equal(t, []string{}, validateAllowlists(opts)) +} + +func Test_validateRoutes(t *testing.T) { + testCases := map[string]struct { + Regexes []string + Expected []string + }{ + "Valid regex routes": { + Regexes: []string{ + "/foo", + "POST=/foo/bar", + "PUT=^/foo/bar$", + "DELETE=/crazy/(?:regex)?/[^/]+/stuff$", + }, + Expected: []string{}, + }, + "Bad regexes do not compile": { + Regexes: []string{ + "POST=/(foo", + "OPTIONS=/foo/bar)", + "GET=^]/foo/bar[$", + "GET=^]/foo/bar[$", + }, + Expected: []string{ + "error compiling regex //(foo/: error parsing regexp: missing closing ): `/(foo`", + "error compiling regex //foo/bar)/: error parsing regexp: unexpected ): `/foo/bar)`", + "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", + "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", + }, + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + opts := &options.Options{ + SkipAuthRoutes: tc.Regexes, + } + msgs := validateRoutes(opts) + assert.Equal(t, tc.Expected, msgs) + }) + } +} + +func Test_validateRegexes(t *testing.T) { + testCases := map[string]struct { + Regexes []string + Expected []string + }{ + "Valid regex routes": { + Regexes: []string{ + "/foo", + "/foo/bar", + "^/foo/bar$", + "/crazy/(?:regex)?/[^/]+/stuff$", + }, + Expected: []string{}, + }, + "Bad regexes do not compile": { + Regexes: []string{ + "/(foo", + "/foo/bar)", + "^]/foo/bar[$", + "^]/foo/bar[$", + }, + Expected: []string{ + "error compiling regex //(foo/: error parsing regexp: missing closing ): `/(foo`", + "error compiling regex //foo/bar)/: error parsing regexp: unexpected ): `/foo/bar)`", + "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", + "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", + }, + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + opts := &options.Options{ + SkipAuthRegex: tc.Regexes, + } + msgs := validateRegexes(opts) + assert.Equal(t, tc.Expected, msgs) + }) + } +} + +func Test_validateTrustedIPs(t *testing.T) { + testCases := map[string]struct { + TrustedIPs []string + Expected []string + }{ + "Non-overlapping valid IPs": { + TrustedIPs: []string{ + "127.0.0.1", + "10.32.0.1/32", + "43.36.201.0/24", + "::1", + "2a12:105:ee7:9234:0:0:0:0/64", + }, + Expected: []string{}, + }, + "Overlapping valid IPs": { + TrustedIPs: []string{ + "135.180.78.199", + "135.180.78.199/32", + "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4", + "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4/128", + }, + Expected: []string{}, + }, + "Invalid IPs": { + TrustedIPs: []string{"[::1]", "alkwlkbn/32"}, + Expected: []string{ + "trusted_ips[0] ([::1]) could not be recognized", + "trusted_ips[1] (alkwlkbn/32) could not be recognized", + }, + }, + } + + for testName, tc := range testCases { + t.Run(testName, func(t *testing.T) { + opts := &options.Options{ + TrustedIPs: tc.TrustedIPs, + } + msgs := validateTrustedIPs(opts) + assert.Equal(t, tc.Expected, msgs) + }) + } +} diff --git a/pkg/validation/options.go b/pkg/validation/options.go index 12631eb9..2ea35f9f 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -9,7 +9,6 @@ import ( "net/http" "net/url" "os" - "regexp" "strings" "github.com/coreos/go-oidc" @@ -184,15 +183,6 @@ func Validate(o *options.Options) error { o.SetRedirectURL(redirectURL) msgs = append(msgs, validateUpstreams(o.UpstreamServers)...) - - for _, u := range o.SkipAuthRegex { - compiledRegex, err := regexp.Compile(u) - if err != nil { - msgs = append(msgs, fmt.Sprintf("error compiling regex=%q %s", u, err)) - continue - } - o.SetCompiledRegex(append(o.GetCompiledRegex(), compiledRegex)) - } msgs = parseProviderInfo(o, msgs) if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" { @@ -223,18 +213,8 @@ func Validate(o *options.Options) error { }) } - if len(o.TrustedIPs) > 0 && o.ReverseProxy { - _, err := fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.") - if err != nil { - panic(err) - } - } - - for i, ipStr := range o.TrustedIPs { - if nil == ip.ParseIPNet(ipStr) { - msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr)) - } - } + // Do this after ReverseProxy validation for TrustedIP coordinated checks + msgs = append(msgs, validateAllowlists(o)...) if len(msgs) != 0 { return fmt.Errorf("invalid configuration:\n %s", diff --git a/pkg/validation/options_test.go b/pkg/validation/options_test.go index 1f418f82..f88ef7af 100644 --- a/pkg/validation/options_test.go +++ b/pkg/validation/options_test.go @@ -2,7 +2,6 @@ package validation import ( "crypto" - "errors" "io/ioutil" "net/url" "os" @@ -78,12 +77,19 @@ func TestClientSecretFileOption(t *testing.T) { if err != nil { t.Fatalf("failed to create temp file: %v", err) } - f.WriteString("testcase") + _, err = f.WriteString("testcase") + if err != nil { + t.Fatalf("failed to write to temp file: %v", err) + } if err := f.Close(); err != nil { t.Fatalf("failed to close temp file: %v", err) } clientSecretFileName := f.Name() - defer os.Remove(clientSecretFileName) + defer func(t *testing.T) { + if err := os.Remove(clientSecretFileName); err != nil { + t.Fatalf("failed to delete temp file: %v", err) + } + }(t) o := options.NewOptions() o.Cookie.Secret = cookieSecret @@ -144,41 +150,6 @@ func TestRedirectURL(t *testing.T) { assert.Equal(t, expected, o.GetRedirectURL()) } -func TestCompiledRegex(t *testing.T) { - o := testOptions() - regexps := []string{"/foo/.*", "/ba[rz]/quux"} - o.SkipAuthRegex = regexps - assert.Equal(t, nil, Validate(o)) - actual := make([]string, 0) - for _, regex := range o.GetCompiledRegex() { - actual = append(actual, regex.String()) - } - assert.Equal(t, regexps, actual) -} - -func TestCompiledRegexError(t *testing.T) { - o := testOptions() - o.SkipAuthRegex = []string{"(foobaz", "barquux)"} - err := Validate(o) - assert.NotEqual(t, nil, err) - - expected := errorMsg([]string{ - "error compiling regex=\"(foobaz\" error parsing regexp: " + - "missing closing ): `(foobaz`", - "error compiling regex=\"barquux)\" error parsing regexp: " + - "unexpected ): `barquux)`"}) - assert.Equal(t, expected, err.Error()) - - o.SkipAuthRegex = []string{"foobaz", "barquux)"} - err = Validate(o) - assert.NotEqual(t, nil, err) - - expected = errorMsg([]string{ - "error compiling regex=\"barquux)\" error parsing regexp: " + - "unexpected ): `barquux)`"}) - assert.Equal(t, expected, err.Error()) -} - func TestDefaultProviderApiSettings(t *testing.T) { o := testOptions() assert.Equal(t, nil, Validate(o)) @@ -337,45 +308,6 @@ func TestRealClientIPHeader(t *testing.T) { assert.Nil(t, o.GetRealClientIPParser()) } -func TestIPCIDRSetOption(t *testing.T) { - tests := []struct { - name string - trustedIPs []string - err error - }{ - { - "TestSomeIPs", - []string{"127.0.0.1", "10.32.0.1/32", "43.36.201.0/24", "::1", "2a12:105:ee7:9234:0:0:0:0/64"}, - nil, - }, { - "TestOverlappingIPs", - []string{"135.180.78.199", "135.180.78.199/32", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4/128"}, - nil, - }, { - "TestInvalidIPs", - []string{"[::1]", "alkwlkbn/32"}, - errors.New( - "invalid configuration:\n" + - " trusted_ips[0] ([::1]) could not be recognized\n" + - " trusted_ips[1] (alkwlkbn/32) could not be recognized", - ), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := testOptions() - o.TrustedIPs = tt.trustedIPs - err := Validate(o) - if tt.err == nil { - assert.Nil(t, err) - } else { - assert.Equal(t, tt.err.Error(), err.Error()) - } - }) - } -} - func TestProviderCAFilesError(t *testing.T) { file, err := ioutil.TempFile("", "absent.*.crt") assert.NoError(t, err) From cfd3de807c1ea4bc072203438e8239e65040e862 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Wed, 23 Sep 2020 20:16:05 -0700 Subject: [PATCH 08/17] Add tests for skip auth functionality --- oauthproxy.go | 2 +- oauthproxy_test.go | 273 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 254 insertions(+), 21 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index ddeafdcd..c92eb850 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -286,7 +286,7 @@ func buildSignInMessage(opts *options.Options) string { // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option // (method=path support) func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { - var routes []*allowedRoute + routes := make([]*allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) for _, path := range opts.SkipAuthRegex { compiledRegex, err := regexp.Compile(path) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index d0feec2d..53bc9543 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -1482,28 +1482,28 @@ func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) { } func TestAuthSkippedForPreflightRequests(t *testing.T) { - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) _, err := w.Write([]byte("response")) if err != nil { t.Fatal(err) } })) - t.Cleanup(upstream.Close) + t.Cleanup(upstreamServer.Close) opts := baseTestOptions() opts.UpstreamServers = options.Upstreams{ { - ID: upstream.URL, + ID: upstreamServer.URL, Path: "/", - URI: upstream.URL, + URI: upstreamServer.URL, }, } opts.SkipAuthPreflight = true err := validation.Validate(opts) assert.NoError(t, err) - upstreamURL, _ := url.Parse(upstream.URL) + upstreamURL, _ := url.Parse(upstreamServer.URL) opts.SetProvider(NewTestProvider(upstreamURL, "")) proxy, err := NewOAuthProxy(opts, func(string) bool { return false }) @@ -1561,17 +1561,17 @@ func NewSignatureTest() (*SignatureTest, error) { opts.EmailDomains = []string{"acm.org"} authenticator := &SignatureAuthenticator{} - upstream := httptest.NewServer( + upstreamServer := httptest.NewServer( http.HandlerFunc(authenticator.Authenticate)) - upstreamURL, err := url.Parse(upstream.URL) + upstreamURL, err := url.Parse(upstreamServer.URL) if err != nil { return nil, err } opts.UpstreamServers = options.Upstreams{ { - ID: upstream.URL, + ID: upstreamServer.URL, Path: "/", - URI: upstream.URL, + URI: upstreamServer.URL, }, } @@ -1590,7 +1590,7 @@ func NewSignatureTest() (*SignatureTest, error) { return &SignatureTest{ opts, - upstream, + upstreamServer, upstreamURL.Host, provider, make(http.Header), @@ -1974,20 +1974,20 @@ func Test_prepareNoCache(t *testing.T) { } func Test_noCacheHeaders(t *testing.T) { - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("upstream")) if err != nil { t.Error(err) } })) - t.Cleanup(upstream.Close) + t.Cleanup(upstreamServer.Close) opts := baseTestOptions() opts.UpstreamServers = options.Upstreams{ { - ID: upstream.URL, + ID: upstreamServer.URL, Path: "/", - URI: upstream.URL, + URI: upstreamServer.URL, }, } opts.SkipAuthRegex = []string{".*"} @@ -2224,7 +2224,8 @@ func TestTrustedIPs(t *testing.T) { opts.TrustedIPs = tt.trustedIPs opts.ReverseProxy = tt.reverseProxy opts.RealClientIPHeader = tt.realClientIPHeader - validation.Validate(opts) + err := validation.Validate(opts) + assert.NoError(t, err) proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) assert.NoError(t, err) @@ -2240,6 +2241,237 @@ func TestTrustedIPs(t *testing.T) { } } +func Test_buildRoutesAllowlist(t *testing.T) { + testCases := []struct { + name string + skipAuthRegex []string + skipAuthRoutes []string + expectedMethods []string + expectedRegexes []string + shouldError bool + }{ + { + name: "No skip auth configured", + skipAuthRegex: []string{}, + skipAuthRoutes: []string{}, + expectedMethods: []string{}, + expectedRegexes: []string{}, + shouldError: false, + }, + { + name: "Only skipAuthRegex configured", + skipAuthRegex: []string{ + "^/foo/bar", + "^/baz/[0-9]+/thing", + }, + skipAuthRoutes: []string{}, + expectedMethods: []string{ + "", + "", + }, + expectedRegexes: []string{ + "^/foo/bar", + "^/baz/[0-9]+/thing", + }, + shouldError: false, + }, + { + name: "Only skipAuthRoutes configured", + skipAuthRegex: []string{}, + skipAuthRoutes: []string{ + "GET=^/foo/bar", + "POST=^/baz/[0-9]+/thing", + "^/all/methods$", + "WEIRD=^/methods/are/allowed", + "PATCH=/second/equals?are=handled&just=fine", + }, + expectedMethods: []string{ + "GET", + "POST", + "", + "WEIRD", + "PATCH", + }, + expectedRegexes: []string{ + "^/foo/bar", + "^/baz/[0-9]+/thing", + "^/all/methods$", + "^/methods/are/allowed", + "/second/equals?are=handled&just=fine", + }, + shouldError: false, + }, + { + name: "Both skipAuthRegexes and skipAuthRoutes configured", + skipAuthRegex: []string{ + "^/foo/bar/regex", + "^/baz/[0-9]+/thing/regex", + }, + skipAuthRoutes: []string{ + "GET=^/foo/bar", + "POST=^/baz/[0-9]+/thing", + "^/all/methods$", + }, + expectedMethods: []string{ + "", + "", + "GET", + "POST", + "", + }, + expectedRegexes: []string{ + "^/foo/bar/regex", + "^/baz/[0-9]+/thing/regex", + "^/foo/bar", + "^/baz/[0-9]+/thing", + "^/all/methods$", + }, + shouldError: false, + }, + { + name: "Invalid skipAuthRegex entry", + skipAuthRegex: []string{ + "^/foo/bar", + "^/baz/[0-9]+/thing", + "(bad[regex", + }, + skipAuthRoutes: []string{}, + expectedMethods: []string{}, + expectedRegexes: []string{}, + shouldError: true, + }, + { + name: "Invalid skipAuthRoutes entry", + skipAuthRegex: []string{}, + skipAuthRoutes: []string{ + "GET=^/foo/bar", + "POST=^/baz/[0-9]+/thing", + "^/all/methods$", + "PUT=(bad[regex", + }, + expectedMethods: []string{}, + expectedRegexes: []string{}, + shouldError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := &options.Options{ + SkipAuthRegex: tc.skipAuthRegex, + SkipAuthRoutes: tc.skipAuthRoutes, + } + routes, err := buildRoutesAllowlist(opts) + if tc.shouldError { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + for i, route := range routes { + assert.Greater(t, len(tc.expectedMethods), i) + assert.Equal(t, route.method, tc.expectedMethods[i]) + assert.Greater(t, len(tc.expectedRegexes), i) + assert.Equal(t, route.pathRegex.String(), tc.expectedRegexes[i]) + } + }) + } +} + +func TestAllowedRequest(t *testing.T) { + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, err := w.Write([]byte("Allowed Request")) + if err != nil { + t.Fatal(err) + } + })) + t.Cleanup(upstreamServer.Close) + + opts := baseTestOptions() + opts.UpstreamServers = options.Upstreams{ + { + ID: upstreamServer.URL, + Path: "/", + URI: upstreamServer.URL, + }, + } + opts.SkipAuthRegex = []string{ + "^/skip/auth/regex$", + } + opts.SkipAuthRoutes = []string{ + "GET=^/skip/auth/routes/get", + } + err := validation.Validate(opts) + assert.NoError(t, err) + proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + name string + method string + url string + allowed bool + }{ + { + name: "Regex GET allowed", + method: "GET", + url: "/skip/auth/regex", + allowed: true, + }, + { + name: "Regex POST allowed ", + method: "POST", + url: "/skip/auth/regex", + allowed: true, + }, + { + name: "Regex denied", + method: "GET", + url: "/wrong/denied", + allowed: false, + }, + { + name: "Route allowed", + method: "GET", + url: "/skip/auth/routes/get", + allowed: true, + }, + { + name: "Route denied with wrong method", + method: "PATCH", + url: "/skip/auth/routes/get", + allowed: false, + }, + { + name: "Route denied with wrong path", + method: "GET", + url: "/skip/auth/routes/wrong/path", + allowed: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(tc.method, tc.url, nil) + assert.NoError(t, err) + assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req)) + + rw := httptest.NewRecorder() + proxy.ServeHTTP(rw, req) + + if tc.allowed { + assert.Equal(t, 200, rw.Code) + assert.Equal(t, "Allowed Request", rw.Body.String()) + } else { + assert.Equal(t, 403, rw.Code) + } + }) + } +} + func TestProxyAllowedGroups(t *testing.T) { tests := []struct { name string @@ -2265,18 +2497,18 @@ func TestProxyAllowedGroups(t *testing.T) { CreatedAt: &created, } - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })) - t.Cleanup(upstream.Close) + t.Cleanup(upstreamServer.Close) test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { opts.AllowedGroups = tt.allowedGroups opts.UpstreamServers = options.Upstreams{ { - ID: upstream.URL, + ID: upstreamServer.URL, Path: "/", - URI: upstream.URL, + URI: upstreamServer.URL, }, } }) @@ -2287,7 +2519,8 @@ func TestProxyAllowedGroups(t *testing.T) { test.req, _ = http.NewRequest("GET", "/", nil) test.req.Header.Add("accept", applicationJSON) - test.SaveSession(session) + err = test.SaveSession(session) + assert.NoError(t, err) test.proxy.ServeHTTP(test.rw, test.req) if tt.expectUnauthorized { From fa4ba5e7ea9387481a641e1e042596092b45b00c Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Wed, 23 Sep 2020 20:37:58 -0700 Subject: [PATCH 09/17] Convert allowlist validation test to Ginkgo --- CHANGELOG.md | 6 +- pkg/validation/allowlist_test.go | 157 +++++++++++++------------------ 2 files changed, 69 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9019068..c3037b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,12 @@ ## Important Notes -- [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Sessions from v5.1.1 or earlier will no longer validate since they were not signed with SHA1. - - Sessions from v6.0.0 or later had a graceful conversion to SHA256 that resulted in no reauthentication - - Upgrading from v5.1.1 or earlier will result in a reauthentication - [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex` - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version. - If your regex contains an `=` and you want it for all methods, you will need to add a leading `=` (this is the area where `--skip-auth-regex` doesn't port perfectly) +- [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Sessions from v5.1.1 or earlier will no longer validate since they were not signed with SHA1. + - Sessions from v6.0.0 or later had a graceful conversion to SHA256 that resulted in no reauthentication + - Upgrading from v5.1.1 or earlier will result in a reauthentication - [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Ensure you have configured oauth2-proxy to use the `groups` scope. The user may be logged out initially as they may not currently have the `groups` claim however after going back through login process wil be authenticated. ## Breaking Changes diff --git a/pkg/validation/allowlist_test.go b/pkg/validation/allowlist_test.go index a17bb12d..1dd48288 100644 --- a/pkg/validation/allowlist_test.go +++ b/pkg/validation/allowlist_test.go @@ -1,149 +1,124 @@ package validation import ( - "testing" - "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" - "github.com/stretchr/testify/assert" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" ) -func Test_validateAllowlists(t *testing.T) { - opts := &options.Options{ - SkipAuthRoutes: []string{ - "POST=/foo/bar", - "PUT=^/foo/bar$", - }, - SkipAuthRegex: []string{"/foo/baz"}, - TrustedIPs: []string{ - "10.32.0.1/32", - "43.36.201.0/24", - }, +var _ = Describe("Allowlist", func() { + type validateRoutesTableInput struct { + routes []string + errStrings []string } - assert.Equal(t, []string{}, validateAllowlists(opts)) -} -func Test_validateRoutes(t *testing.T) { - testCases := map[string]struct { - Regexes []string - Expected []string - }{ - "Valid regex routes": { - Regexes: []string{ + type validateRegexesTableInput struct { + regexes []string + errStrings []string + } + + type validateTrustedIPsTableInput struct { + trustedIPs []string + errStrings []string + } + + DescribeTable("validateRoutes", + func(r *validateRoutesTableInput) { + opts := &options.Options{ + SkipAuthRoutes: r.routes, + } + Expect(validateRoutes(opts)).To(ConsistOf(r.errStrings)) + }, + Entry("Valid regex routes", &validateRoutesTableInput{ + routes: []string{ "/foo", "POST=/foo/bar", "PUT=^/foo/bar$", "DELETE=/crazy/(?:regex)?/[^/]+/stuff$", }, - Expected: []string{}, - }, - "Bad regexes do not compile": { - Regexes: []string{ + errStrings: []string{}, + }), + Entry("Bad regexes do not compile", &validateRoutesTableInput{ + routes: []string{ "POST=/(foo", "OPTIONS=/foo/bar)", "GET=^]/foo/bar[$", "GET=^]/foo/bar[$", }, - Expected: []string{ + errStrings: []string{ "error compiling regex //(foo/: error parsing regexp: missing closing ): `/(foo`", "error compiling regex //foo/bar)/: error parsing regexp: unexpected ): `/foo/bar)`", "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", }, - }, - } + }), + ) - for testName, tc := range testCases { - t.Run(testName, func(t *testing.T) { + DescribeTable("validateRegexes", + func(r *validateRegexesTableInput) { opts := &options.Options{ - SkipAuthRoutes: tc.Regexes, + SkipAuthRegex: r.regexes, } - msgs := validateRoutes(opts) - assert.Equal(t, tc.Expected, msgs) - }) - } -} - -func Test_validateRegexes(t *testing.T) { - testCases := map[string]struct { - Regexes []string - Expected []string - }{ - "Valid regex routes": { - Regexes: []string{ + Expect(validateRegexes(opts)).To(ConsistOf(r.errStrings)) + }, + Entry("Valid regex routes", &validateRegexesTableInput{ + regexes: []string{ "/foo", "/foo/bar", "^/foo/bar$", "/crazy/(?:regex)?/[^/]+/stuff$", }, - Expected: []string{}, - }, - "Bad regexes do not compile": { - Regexes: []string{ + errStrings: []string{}, + }), + Entry("Bad regexes do not compile", &validateRegexesTableInput{ + regexes: []string{ "/(foo", "/foo/bar)", "^]/foo/bar[$", "^]/foo/bar[$", }, - Expected: []string{ + errStrings: []string{ "error compiling regex //(foo/: error parsing regexp: missing closing ): `/(foo`", "error compiling regex //foo/bar)/: error parsing regexp: unexpected ): `/foo/bar)`", "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", "error compiling regex /^]/foo/bar[$/: error parsing regexp: missing closing ]: `[$`", }, - }, - } + }), + ) - for testName, tc := range testCases { - t.Run(testName, func(t *testing.T) { + DescribeTable("validateTrustedIPs", + func(t *validateTrustedIPsTableInput) { opts := &options.Options{ - SkipAuthRegex: tc.Regexes, + TrustedIPs: t.trustedIPs, } - msgs := validateRegexes(opts) - assert.Equal(t, tc.Expected, msgs) - }) - } -} - -func Test_validateTrustedIPs(t *testing.T) { - testCases := map[string]struct { - TrustedIPs []string - Expected []string - }{ - "Non-overlapping valid IPs": { - TrustedIPs: []string{ + Expect(validateTrustedIPs(opts)).To(ConsistOf(t.errStrings)) + }, + Entry("Non-overlapping valid IPs", &validateTrustedIPsTableInput{ + trustedIPs: []string{ "127.0.0.1", "10.32.0.1/32", "43.36.201.0/24", "::1", "2a12:105:ee7:9234:0:0:0:0/64", }, - Expected: []string{}, - }, - "Overlapping valid IPs": { - TrustedIPs: []string{ + errStrings: []string{}, + }), + Entry("Overlapping valid IPs", &validateTrustedIPsTableInput{ + trustedIPs: []string{ "135.180.78.199", "135.180.78.199/32", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4", "d910:a5a1:16f8:ddf5:e5b9:5cef:a65e:41f4/128", }, - Expected: []string{}, - }, - "Invalid IPs": { - TrustedIPs: []string{"[::1]", "alkwlkbn/32"}, - Expected: []string{ + errStrings: []string{}, + }), + Entry("Invalid IPs", &validateTrustedIPsTableInput{ + trustedIPs: []string{"[::1]", "alkwlkbn/32"}, + errStrings: []string{ "trusted_ips[0] ([::1]) could not be recognized", "trusted_ips[1] (alkwlkbn/32) could not be recognized", }, - }, - } - - for testName, tc := range testCases { - t.Run(testName, func(t *testing.T) { - opts := &options.Options{ - TrustedIPs: tc.TrustedIPs, - } - msgs := validateTrustedIPs(opts) - assert.Equal(t, tc.Expected, msgs) - }) - } -} + }), + ) +}) From 89a8ac8c1f4995e270b8db3f6b71b28fc95ef780 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sat, 26 Sep 2020 12:38:01 -0700 Subject: [PATCH 10/17] Add startup logging for skipped auth routes --- oauthproxy.go | 2 ++ oauthproxy_test.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index c92eb850..ae5231f6 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -293,6 +293,7 @@ func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { if err != nil { return nil, err } + logger.Printf("Skipping auth - Method: ALL | Path: %s", path) routes = append(routes, &allowedRoute{ method: "", pathRegex: compiledRegex, @@ -318,6 +319,7 @@ func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { if err != nil { return nil, err } + logger.Printf("Skipping auth - Method: %s | Path: %s", method, path) routes = append(routes, &allowedRoute{ method: method, pathRegex: compiledRegex, diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 53bc9543..e42033d1 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -2365,9 +2365,9 @@ func Test_buildRoutesAllowlist(t *testing.T) { if tc.shouldError { assert.Error(t, err) return - } else { - assert.NoError(t, err) } + assert.NoError(t, err) + for i, route := range routes { assert.Greater(t, len(tc.expectedMethods), i) assert.Equal(t, route.method, tc.expectedMethods[i]) From b7b7ade7c4aaa95f2da051961c3bda6d7e8103d5 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 5 Oct 2020 12:39:44 -0700 Subject: [PATCH 11/17] Improve AllowedRoute test table formatting --- oauthproxy.go | 14 ++-- oauthproxy_test.go | 130 ++++++++++++++++++------------- pkg/validation/allowlist.go | 8 +- pkg/validation/allowlist_test.go | 3 +- 4 files changed, 87 insertions(+), 68 deletions(-) diff --git a/oauthproxy.go b/oauthproxy.go index ae5231f6..e64ffe91 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -76,7 +76,7 @@ type OAuthProxy struct { AuthOnlyPath string UserInfoPath string - allowedRoutes []*allowedRoute + allowedRoutes []allowedRoute redirectURL *url.URL // the url to receive requests at whitelistDomains []string provider providers.Provider @@ -285,8 +285,8 @@ func buildSignInMessage(opts *options.Options) string { // buildRoutesAllowlist builds an []allowedRoute list from either the legacy // SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option // (method=path support) -func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { - routes := make([]*allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) +func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { + routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) for _, path := range opts.SkipAuthRegex { compiledRegex, err := regexp.Compile(path) @@ -294,7 +294,7 @@ func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { return nil, err } logger.Printf("Skipping auth - Method: ALL | Path: %s", path) - routes = append(routes, &allowedRoute{ + routes = append(routes, allowedRoute{ method: "", pathRegex: compiledRegex, }) @@ -306,13 +306,13 @@ func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { path string ) - parts := strings.Split(methodPath, "=") + parts := strings.SplitN(methodPath, "=", 2) if len(parts) == 1 { method = "" path = parts[0] } else { method = strings.ToUpper(parts[0]) - path = strings.Join(parts[1:], "=") + path = parts[1] } compiledRegex, err := regexp.Compile(path) @@ -320,7 +320,7 @@ func buildRoutesAllowlist(opts *options.Options) ([]*allowedRoute, error) { return nil, err } logger.Printf("Skipping auth - Method: %s | Path: %s", method, path) - routes = append(routes, &allowedRoute{ + routes = append(routes, allowedRoute{ method: method, pathRegex: compiledRegex, }) diff --git a/oauthproxy_test.go b/oauthproxy_test.go index e42033d1..d0fd9481 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -2242,21 +2242,24 @@ func TestTrustedIPs(t *testing.T) { } func Test_buildRoutesAllowlist(t *testing.T) { + type expectedAllowedRoute struct { + method string + regexString string + } + testCases := []struct { - name string - skipAuthRegex []string - skipAuthRoutes []string - expectedMethods []string - expectedRegexes []string - shouldError bool + name string + skipAuthRegex []string + skipAuthRoutes []string + expectedRoutes []expectedAllowedRoute + shouldError bool }{ { - name: "No skip auth configured", - skipAuthRegex: []string{}, - skipAuthRoutes: []string{}, - expectedMethods: []string{}, - expectedRegexes: []string{}, - shouldError: false, + name: "No skip auth configured", + skipAuthRegex: []string{}, + skipAuthRoutes: []string{}, + expectedRoutes: []expectedAllowedRoute{}, + shouldError: false, }, { name: "Only skipAuthRegex configured", @@ -2265,13 +2268,15 @@ func Test_buildRoutesAllowlist(t *testing.T) { "^/baz/[0-9]+/thing", }, skipAuthRoutes: []string{}, - expectedMethods: []string{ - "", - "", - }, - expectedRegexes: []string{ - "^/foo/bar", - "^/baz/[0-9]+/thing", + expectedRoutes: []expectedAllowedRoute{ + { + method: "", + regexString: "^/foo/bar", + }, + { + method: "", + regexString: "^/baz/[0-9]+/thing", + }, }, shouldError: false, }, @@ -2285,19 +2290,27 @@ func Test_buildRoutesAllowlist(t *testing.T) { "WEIRD=^/methods/are/allowed", "PATCH=/second/equals?are=handled&just=fine", }, - expectedMethods: []string{ - "GET", - "POST", - "", - "WEIRD", - "PATCH", - }, - expectedRegexes: []string{ - "^/foo/bar", - "^/baz/[0-9]+/thing", - "^/all/methods$", - "^/methods/are/allowed", - "/second/equals?are=handled&just=fine", + expectedRoutes: []expectedAllowedRoute{ + { + method: "GET", + regexString: "^/foo/bar", + }, + { + method: "POST", + regexString: "^/baz/[0-9]+/thing", + }, + { + method: "", + regexString: "^/all/methods$", + }, + { + method: "WEIRD", + regexString: "^/methods/are/allowed", + }, + { + method: "PATCH", + regexString: "/second/equals?are=handled&just=fine", + }, }, shouldError: false, }, @@ -2312,19 +2325,27 @@ func Test_buildRoutesAllowlist(t *testing.T) { "POST=^/baz/[0-9]+/thing", "^/all/methods$", }, - expectedMethods: []string{ - "", - "", - "GET", - "POST", - "", - }, - expectedRegexes: []string{ - "^/foo/bar/regex", - "^/baz/[0-9]+/thing/regex", - "^/foo/bar", - "^/baz/[0-9]+/thing", - "^/all/methods$", + expectedRoutes: []expectedAllowedRoute{ + { + method: "", + regexString: "^/foo/bar/regex", + }, + { + method: "", + regexString: "^/baz/[0-9]+/thing/regex", + }, + { + method: "GET", + regexString: "^/foo/bar", + }, + { + method: "POST", + regexString: "^/baz/[0-9]+/thing", + }, + { + method: "", + regexString: "^/all/methods$", + }, }, shouldError: false, }, @@ -2335,10 +2356,9 @@ func Test_buildRoutesAllowlist(t *testing.T) { "^/baz/[0-9]+/thing", "(bad[regex", }, - skipAuthRoutes: []string{}, - expectedMethods: []string{}, - expectedRegexes: []string{}, - shouldError: true, + skipAuthRoutes: []string{}, + expectedRoutes: []expectedAllowedRoute{}, + shouldError: true, }, { name: "Invalid skipAuthRoutes entry", @@ -2349,9 +2369,8 @@ func Test_buildRoutesAllowlist(t *testing.T) { "^/all/methods$", "PUT=(bad[regex", }, - expectedMethods: []string{}, - expectedRegexes: []string{}, - shouldError: true, + expectedRoutes: []expectedAllowedRoute{}, + shouldError: true, }, } @@ -2369,10 +2388,9 @@ func Test_buildRoutesAllowlist(t *testing.T) { assert.NoError(t, err) for i, route := range routes { - assert.Greater(t, len(tc.expectedMethods), i) - assert.Equal(t, route.method, tc.expectedMethods[i]) - assert.Greater(t, len(tc.expectedRegexes), i) - assert.Equal(t, route.pathRegex.String(), tc.expectedRegexes[i]) + assert.Greater(t, len(tc.expectedRoutes), i) + assert.Equal(t, route.method, tc.expectedRoutes[i].method) + assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString) } }) } diff --git a/pkg/validation/allowlist.go b/pkg/validation/allowlist.go index 254983d8..56a3fd4c 100644 --- a/pkg/validation/allowlist.go +++ b/pkg/validation/allowlist.go @@ -6,8 +6,8 @@ import ( "regexp" "strings" - "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" - "github.com/oauth2-proxy/oauth2-proxy/pkg/ip" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip" ) func validateAllowlists(o *options.Options) []string { @@ -32,11 +32,11 @@ func validateRoutes(o *options.Options) []string { msgs := []string{} for _, route := range o.SkipAuthRoutes { var regex string - parts := strings.Split(route, "=") + parts := strings.SplitN(route, "=", 2) if len(parts) == 1 { regex = parts[0] } else { - regex = strings.Join(parts[1:], "=") + regex = parts[1] } _, err := regexp.Compile(regex) if err != nil { diff --git a/pkg/validation/allowlist_test.go b/pkg/validation/allowlist_test.go index 1dd48288..4600a718 100644 --- a/pkg/validation/allowlist_test.go +++ b/pkg/validation/allowlist_test.go @@ -1,10 +1,11 @@ package validation import ( - "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" ) var _ = Describe("Allowlist", func() { From eec7565c52a6b8b0d4f5d3cde43f9e9483002c48 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Thu, 23 Jul 2020 10:05:59 +0100 Subject: [PATCH 12/17] Add Header option structure --- pkg/apis/options/common.go | 14 ++++++++++++ pkg/apis/options/header.go | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 pkg/apis/options/common.go create mode 100644 pkg/apis/options/header.go diff --git a/pkg/apis/options/common.go b/pkg/apis/options/common.go new file mode 100644 index 00000000..60d352a5 --- /dev/null +++ b/pkg/apis/options/common.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 + + // FromEnv expects the name of an environment variable. + FromEnv string + + // FromFile expects a path to a file containing the secret value. + FromFile string +} diff --git a/pkg/apis/options/header.go b/pkg/apis/options/header.go new file mode 100644 index 00000000..0b2e1b69 --- /dev/null +++ b/pkg/apis/options/header.go @@ -0,0 +1,44 @@ +package options + +// Header represents an individual header that will be added to a request or +// response header. +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"` + + // PreserveRequestValue determines whether any values for this header + // should be preserved for the request to the upstream server. + // This option only takes effet on injected request headers. + // Defaults to false (headers that match this header will be stripped). + PreserveRequestValue bool `json:"preserveRequestValue"` + + // Values contains the desired values for this header + Values []HeaderValue `json:"values"` +} + +// 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 + + // Allow users to load the value from a session claim + *ClaimSource +} + +// ClaimSource allows loading a header value from a claim within the session +type ClaimSource struct { + // Claim is the name of the claim in the session that the value should be + // loaded from. + Claim string `json:"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"` + + // 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 +} From fc2ff19a1908e43f368792e2a57518beb2add7f5 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Sun, 26 Jul 2020 04:50:18 +0100 Subject: [PATCH 13/17] Add header Injector --- pkg/apis/options/util/util.go | 26 ++ pkg/apis/options/util/util_suite_test.go | 16 + pkg/apis/options/util/util_test.go | 88 +++++ pkg/apis/sessions/session_state.go | 29 ++ pkg/header/header_suite_test.go | 37 ++ pkg/header/injector.go | 112 ++++++ pkg/header/injector_test.go | 417 +++++++++++++++++++++++ 7 files changed, 725 insertions(+) create mode 100644 pkg/apis/options/util/util.go create mode 100644 pkg/apis/options/util/util_suite_test.go create mode 100644 pkg/apis/options/util/util_test.go create mode 100644 pkg/header/header_suite_test.go create mode 100644 pkg/header/injector.go create mode 100644 pkg/header/injector_test.go diff --git a/pkg/apis/options/util/util.go b/pkg/apis/options/util/util.go new file mode 100644 index 00000000..918da13a --- /dev/null +++ b/pkg/apis/options/util/util.go @@ -0,0 +1,26 @@ +package util + +import ( + "encoding/base64" + "errors" + "io/ioutil" + "os" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" +) + +// GetSecretValue returns the value of the Secret from its source +func GetSecretValue(source *options.SecretSource) ([]byte, error) { + switch { + case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "": + value := make([]byte, base64.StdEncoding.DecodedLen(len(source.Value))) + decoded, err := base64.StdEncoding.Decode(value, source.Value) + return value[:decoded], err + case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "": + return []byte(os.Getenv(source.FromEnv)), nil + case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "": + return ioutil.ReadFile(source.FromFile) + default: + return nil, errors.New("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile") + } +} diff --git a/pkg/apis/options/util/util_suite_test.go b/pkg/apis/options/util/util_suite_test.go new file mode 100644 index 00000000..75f53dbb --- /dev/null +++ b/pkg/apis/options/util/util_suite_test.go @@ -0,0 +1,16 @@ +package util + +import ( + "testing" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestUtilSuite(t *testing.T) { + logger.SetOutput(GinkgoWriter) + + RegisterFailHandler(Fail) + RunSpecs(t, "Options Util Suite") +} diff --git a/pkg/apis/options/util/util_test.go b/pkg/apis/options/util/util_test.go new file mode 100644 index 00000000..5ca76a04 --- /dev/null +++ b/pkg/apis/options/util/util_test.go @@ -0,0 +1,88 @@ +package util + +import ( + "encoding/base64" + "io/ioutil" + "os" + "path" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetSecretValue", func() { + var fileDir string + const secretEnvKey = "SECRET_ENV_KEY" + const secretEnvValue = "secret-env-value" + var secretFileValue = []byte("secret-file-value") + + BeforeEach(func() { + os.Setenv(secretEnvKey, secretEnvValue) + + var err error + fileDir, err = ioutil.TempDir("", "oauth2-proxy-util-get-secret-value") + Expect(err).ToNot(HaveOccurred()) + Expect(ioutil.WriteFile(path.Join(fileDir, "secret-file"), secretFileValue, 0600)).To(Succeed()) + }) + + AfterEach(func() { + os.Unsetenv(secretEnvKey) + os.RemoveAll(fileDir) + }) + + It("returns the correct value from base64", func() { + originalValue := []byte("secret-value-1") + b64Value := base64.StdEncoding.EncodeToString((originalValue)) + + // Once encoded, the originalValue could have a decoded length longer than + // its actual length, ensure we trim this. + // This assertion ensures we are testing the triming + Expect(len(originalValue)).To(BeNumerically("<", base64.StdEncoding.DecodedLen(len(b64Value)))) + + value, err := GetSecretValue(&options.SecretSource{ + Value: []byte(b64Value), + }) + Expect(err).ToNot(HaveOccurred()) + Expect(value).To(Equal(originalValue)) + }) + + It("returns the correct value from the environment", func() { + value, err := GetSecretValue(&options.SecretSource{ + FromEnv: secretEnvKey, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(value).To(BeEquivalentTo(secretEnvValue)) + }) + + It("returns the correct value from a file", func() { + value, err := GetSecretValue(&options.SecretSource{ + FromFile: path.Join(fileDir, "secret-file"), + }) + Expect(err).ToNot(HaveOccurred()) + Expect(value).To(Equal(secretFileValue)) + }) + + It("when the file does not exist", func() { + value, err := GetSecretValue(&options.SecretSource{ + FromFile: path.Join(fileDir, "not-exist"), + }) + Expect(err).To(HaveOccurred()) + Expect(value).To(BeEmpty()) + }) + + It("with no source set", func() { + value, err := GetSecretValue(&options.SecretSource{}) + Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")) + Expect(value).To(BeEmpty()) + }) + + It("with multiple sources set", func() { + value, err := GetSecretValue(&options.SecretSource{ + FromEnv: secretEnvKey, + FromFile: path.Join(fileDir, "secret-file"), + }) + Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")) + Expect(value).To(BeEmpty()) + }) +}) diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index c3db8994..3f675135 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "reflect" + "strings" "time" "unicode/utf8" @@ -69,6 +70,34 @@ func (s *SessionState) String() string { return o + "}" } +func (s *SessionState) GetClaim(claim string) string { + if s == nil { + return "" + } + switch claim { + case "access_token": + return s.AccessToken + case "id_token": + return s.IDToken + case "created_at": + return s.CreatedAt.String() + case "expires_on": + return s.ExpiresOn.String() + case "refresh_token": + return s.RefreshToken + case "email": + return s.Email + case "user": + return s.User + case "groups": + return strings.Join(s.Groups, ",") + case "preferred_username": + return s.PreferredUsername + default: + return "" + } +} + // EncodeSessionState returns an encrypted, lz4 compressed, MessagePack encoded session func (s *SessionState) EncodeSessionState(c encryption.Cipher, compress bool) ([]byte, error) { packed, err := msgpack.Marshal(s) diff --git a/pkg/header/header_suite_test.go b/pkg/header/header_suite_test.go new file mode 100644 index 00000000..3d05cd02 --- /dev/null +++ b/pkg/header/header_suite_test.go @@ -0,0 +1,37 @@ +package header + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var ( + filesDir string +) + +func TestHeaderSuite(t *testing.T) { + logger.SetOutput(GinkgoWriter) + + RegisterFailHandler(Fail) + RunSpecs(t, "Header") +} + +var _ = BeforeSuite(func() { + os.Setenv("SECRET_ENV", "super-secret-env") + + dir, err := ioutil.TempDir("", "oauth2-proxy-header-suite") + Expect(err).ToNot(HaveOccurred()) + Expect(ioutil.WriteFile(path.Join(dir, "secret-file"), []byte("super-secret-file"), 0644)).To(Succeed()) + filesDir = dir +}) + +var _ = AfterSuite(func() { + os.Unsetenv("SECRET_ENV") + Expect(os.RemoveAll(filesDir)).To(Succeed()) +}) diff --git a/pkg/header/injector.go b/pkg/header/injector.go new file mode 100644 index 00000000..136185e5 --- /dev/null +++ b/pkg/header/injector.go @@ -0,0 +1,112 @@ +package header + +import ( + "encoding/base64" + "fmt" + "net/http" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/util" + sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" +) + +type Injector interface { + Inject(http.Header, *sessionsapi.SessionState) +} + +type injector struct { + valueInjectors []valueInjector +} + +func (i injector) Inject(header http.Header, session *sessionsapi.SessionState) { + for _, injector := range i.valueInjectors { + injector.inject(header, session) + } +} + +func NewInjector(headers []options.Header) (Injector, error) { + injectors := []valueInjector{} + for _, header := range headers { + for _, value := range header.Values { + injector, err := newValueinjector(header.Name, value) + if err != nil { + return nil, fmt.Errorf("error building injector for header %q: %v", header.Name, err) + } + injectors = append(injectors, injector) + } + } + + return &injector{valueInjectors: injectors}, nil +} + +type valueInjector interface { + inject(http.Header, *sessionsapi.SessionState) +} + +func newValueinjector(name string, value options.HeaderValue) (valueInjector, error) { + switch { + case value.SecretSource != nil && value.ClaimSource == nil: + return newSecretInjector(name, value.SecretSource) + case value.SecretSource == nil && value.ClaimSource != nil: + return newClaimInjector(name, value.ClaimSource) + default: + return nil, fmt.Errorf("header %q value has multiple entries: only one entry per value is allowed", name) + } +} + +type injectorFunc struct { + injectFunc func(http.Header, *sessionsapi.SessionState) +} + +func (i *injectorFunc) inject(header http.Header, session *sessionsapi.SessionState) { + i.injectFunc(header, session) +} + +func newInjectorFunc(injectFunc func(header http.Header, session *sessionsapi.SessionState)) valueInjector { + return &injectorFunc{injectFunc: injectFunc} +} + +func newSecretInjector(name string, source *options.SecretSource) (valueInjector, error) { + value, err := util.GetSecretValue(source) + if err != nil { + return nil, fmt.Errorf("error getting secret value: %v", err) + } + + return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { + header.Add(name, string(value)) + }), nil +} + +func newClaimInjector(name string, source *options.ClaimSource) (valueInjector, error) { + switch { + case source.BasicAuthPassword != nil: + password, err := util.GetSecretValue(source.BasicAuthPassword) + if err != nil { + return nil, fmt.Errorf("error loading basicAuthPassword: %v", err) + } + return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { + claim := session.GetClaim(source.Claim) + if claim == "" { + return + } + auth := claim + ":" + string(password) + header.Add(name, "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) + }), nil + case source.Prefix != "": + return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { + claim := session.GetClaim(source.Claim) + if claim == "" { + return + } + header.Add(name, source.Prefix+claim) + }), nil + default: + return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { + claim := session.GetClaim(source.Claim) + if claim == "" { + return + } + header.Add(name, claim) + }), nil + } +} diff --git a/pkg/header/injector_test.go b/pkg/header/injector_test.go new file mode 100644 index 00000000..af034fd9 --- /dev/null +++ b/pkg/header/injector_test.go @@ -0,0 +1,417 @@ +package header + +import ( + "encoding/base64" + "errors" + "net/http" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Injector Suite", func() { + Context("NewInjector", func() { + type newInjectorTableInput struct { + headers []options.Header + initialHeaders http.Header + session *sessionsapi.SessionState + expectedHeaders http.Header + expectedErr error + } + + DescribeTable("creates an injector", + func(in newInjectorTableInput) { + injector, err := NewInjector(in.headers) + if in.expectedErr != nil { + Expect(err).To(MatchError(in.expectedErr)) + Expect(injector).To(BeNil()) + return + } + + Expect(err).ToNot(HaveOccurred()) + Expect(injector).ToNot(BeNil()) + + headers := in.initialHeaders.Clone() + injector.Inject(headers, in.session) + Expect(headers).To(Equal(in.expectedHeaders)) + }, + Entry("with no configured headers", newInjectorTableInput{ + headers: []options.Header{}, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + expectedErr: nil, + }), + Entry("with a static valued header from base64", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Secret", + Values: []options.HeaderValue{ + { + SecretSource: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("super-secret"))), + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "Secret": []string{"super-secret"}, + }, + expectedErr: nil, + }), + Entry("with a static valued header from env", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Secret", + Values: []options.HeaderValue{ + { + SecretSource: &options.SecretSource{ + FromEnv: "SECRET_ENV", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "Secret": []string{"super-secret-env"}, + }, + expectedErr: nil, + }), + Entry("with a claim valued header", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "Claim": []string{"IDToken-1234"}, + }, + expectedErr: nil, + }), + Entry("with a claim valued header and a nil session", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: nil, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + expectedErr: nil, + }), + Entry("with a prefixed claim valued header", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + Prefix: "Bearer ", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "Claim": []string{"Bearer IDToken-1234"}, + }, + expectedErr: nil, + }), + Entry("with a prefixed claim valued header missing the claim", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "idToken", + Prefix: "Bearer ", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + expectedErr: nil, + }), + Entry("with a basicAuthPassword and claim valued header", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-Authorization", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + BasicAuthPassword: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + }, + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + User: "user-123", + }, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "X-Auth-Request-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user-123:basic-password"))}, + }, + expectedErr: nil, + }), + Entry("with a basicAuthPassword and claim valued header missing the claim", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-Authorization", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + BasicAuthPassword: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + }, + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + expectedErr: nil, + }), + Entry("with a header that already exists", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-User", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "X-Auth-Request-User": []string{"user"}, + }, + session: &sessionsapi.SessionState{ + User: "user-123", + }, + expectedHeaders: http.Header{ + "X-Auth-Request-User": []string{"user", "user-123"}, + }, + expectedErr: nil, + }), + Entry("with a claim and secret valued header value", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + SecretSource: &options.SecretSource{ + FromEnv: "SECRET_ENV", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: nil, + expectedErr: errors.New("error building injector for header \"Claim\": header \"Claim\" value has multiple entries: only one entry per value is allowed"), + }), + Entry("with an invalid static valued header", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "Secret", + Values: []options.HeaderValue{ + { + SecretSource: &options.SecretSource{ + FromEnv: "SECRET_ENV", + FromFile: "secret-file", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: nil, + expectedErr: errors.New("error building injector for header \"Secret\": error getting secret value: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile"), + }), + Entry("with an invalid basicAuthPassword claim valued header", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-Authorization", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + BasicAuthPassword: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + FromEnv: "SECRET_ENV", + }, + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + User: "user-123", + }, + expectedHeaders: nil, + expectedErr: errors.New("error building injector for header \"X-Auth-Request-Authorization\": error loading basicAuthPassword: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile"), + }), + Entry("with a mix of configured headers", newInjectorTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-Authorization", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + BasicAuthPassword: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + }, + }, + }, + }, + }, + { + Name: "X-Auth-Request-User", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + }, + }, + }, + }, + { + Name: "X-Auth-Request-Email", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "email", + }, + }, + }, + }, + { + Name: "X-Auth-Request-Version-Info", + Values: []options.HeaderValue{ + { + SecretSource: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("major=1"))), + }, + }, + { + SecretSource: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("minor=2"))), + }, + }, + { + SecretSource: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("patch=3"))), + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + User: "user-123", + Email: "user@example.com", + }, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "X-Auth-Request-Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user-123:basic-password"))}, + "X-Auth-Request-User": []string{"user-123"}, + "X-Auth-Request-Email": []string{"user@example.com"}, + "X-Auth-Request-Version-Info": []string{"major=1", "minor=2", "patch=3"}, + }, + expectedErr: nil, + }), + ) + }) +}) From 6743e3991d4a0da3b40ad124877fabfa3234b7a5 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Sun, 26 Jul 2020 04:50:39 +0100 Subject: [PATCH 14/17] Add header injector middlewares --- pkg/middleware/headers.go | 102 +++++++++ pkg/middleware/headers_test.go | 405 +++++++++++++++++++++++++++++++++ 2 files changed, 507 insertions(+) create mode 100644 pkg/middleware/headers.go create mode 100644 pkg/middleware/headers_test.go diff --git a/pkg/middleware/headers.go b/pkg/middleware/headers.go new file mode 100644 index 00000000..6786c2eb --- /dev/null +++ b/pkg/middleware/headers.go @@ -0,0 +1,102 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/justinas/alice" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/header" +) + +func NewRequestHeaderInjector(headers []options.Header) (alice.Constructor, error) { + headerInjector, err := newRequestHeaderInjector(headers) + if err != nil { + return nil, fmt.Errorf("error building request header injector: %v", err) + } + + strip := newStripHeaders(headers) + if strip != nil { + return alice.New(strip, headerInjector).Then, nil + } + return headerInjector, nil +} + +func newStripHeaders(headers []options.Header) alice.Constructor { + headersToStrip := []string{} + for _, header := range headers { + if !header.PreserveRequestValue { + headersToStrip = append(headersToStrip, header.Name) + } + } + + if len(headersToStrip) == 0 { + return nil + } + + return func(next http.Handler) http.Handler { + return stripHeaders(headersToStrip, next) + } +} + +func stripHeaders(headers []string, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + for _, header := range headers { + req.Header.Del(header) + } + next.ServeHTTP(rw, req) + }) +} + +func newRequestHeaderInjector(headers []options.Header) (alice.Constructor, error) { + injector, err := header.NewInjector(headers) + if err != nil { + return nil, fmt.Errorf("error building request injector: %v", err) + } + + return func(next http.Handler) http.Handler { + return injectRequestHeaders(injector, next) + }, nil +} + +func injectRequestHeaders(injector header.Injector, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + scope := GetRequestScope(req) + + // If scope is nil, this will panic. + // A scope should always be injected before this handler is called. + injector.Inject(req.Header, scope.Session) + next.ServeHTTP(rw, req) + }) +} + +func NewResponseHeaderInjector(headers []options.Header) (alice.Constructor, error) { + headerInjector, err := newResponseHeaderInjector(headers) + if err != nil { + return nil, fmt.Errorf("error building response header injector: %v", err) + } + + return headerInjector, nil +} + +func newResponseHeaderInjector(headers []options.Header) (alice.Constructor, error) { + injector, err := header.NewInjector(headers) + if err != nil { + return nil, fmt.Errorf("error building response injector: %v", err) + } + + return func(next http.Handler) http.Handler { + return injectResponseHeaders(injector, next) + }, nil +} + +func injectResponseHeaders(injector header.Injector, next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + scope := GetRequestScope(req) + + // If scope is nil, this will panic. + // A scope should always be injected before this handler is called. + injector.Inject(rw.Header(), scope.Session) + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/middleware/headers_test.go b/pkg/middleware/headers_test.go new file mode 100644 index 00000000..15006b1d --- /dev/null +++ b/pkg/middleware/headers_test.go @@ -0,0 +1,405 @@ +package middleware + +import ( + "context" + "encoding/base64" + "net/http" + "net/http/httptest" + + 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/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Headers Suite", func() { + type headersTableInput struct { + headers []options.Header + initialHeaders http.Header + session *sessionsapi.SessionState + expectedHeaders http.Header + expectedErr string + } + + DescribeTable("the request header injector", + func(in headersTableInput) { + scope := &middlewareapi.RequestScope{ + Session: in.session, + } + + // Set up the request with a request scope + req := httptest.NewRequest("", "/", nil) + contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope) + req = req.WithContext(contextWithScope) + req.Header = in.initialHeaders.Clone() + + rw := httptest.NewRecorder() + + // Create the handler with a next handler that will capture the headers + // from the request + var gotHeaders http.Header + injector, err := NewRequestHeaderInjector(in.headers) + if in.expectedErr != "" { + Expect(err).To(MatchError(in.expectedErr)) + return + } + Expect(err).ToNot(HaveOccurred()) + + handler := injector(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotHeaders = r.Header.Clone() + })) + handler.ServeHTTP(rw, req) + + Expect(gotHeaders).To(Equal(in.expectedHeaders)) + }, + Entry("with no configured headers", headersTableInput{ + headers: []options.Header{}, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + "Claim": []string{"IDToken-1234"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header (without preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "Claim": []string{"IDToken-1234"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header (with preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + PreserveRequestValue: true, + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "Claim": []string{"bar", "baz", "IDToken-1234"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header that's not present (without preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: nil, + expectedHeaders: http.Header{}, + expectedErr: "", + }), + Entry("with a claim valued header that's not present (with preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + PreserveRequestValue: true, + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: nil, + expectedHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + expectedErr: "", + }), + Entry("with an invalid basicAuthPassword claim valued header", headersTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-Authorization", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + BasicAuthPassword: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + FromEnv: "SECRET_ENV", + }, + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + User: "user-123", + }, + expectedHeaders: nil, + expectedErr: "error building request header injector: error building request injector: error building injector for header \"X-Auth-Request-Authorization\": error loading basicAuthPassword: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile", + }), + ) + + DescribeTable("the response header injector", + func(in headersTableInput) { + scope := &middlewareapi.RequestScope{ + Session: in.session, + } + + // Set up the request with a request scope + req := httptest.NewRequest("", "/", nil) + contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope) + req = req.WithContext(contextWithScope) + + rw := httptest.NewRecorder() + for key, values := range in.initialHeaders { + for _, value := range values { + rw.Header().Add(key, value) + } + } + + // Create the handler with a next handler that will capture the headers + // from the request + var gotHeaders http.Header + injector, err := NewResponseHeaderInjector(in.headers) + if in.expectedErr != "" { + Expect(err).To(MatchError(in.expectedErr)) + return + } + Expect(err).ToNot(HaveOccurred()) + + handler := injector(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotHeaders = w.Header().Clone() + })) + handler.ServeHTTP(rw, req) + + Expect(gotHeaders).To(Equal(in.expectedHeaders)) + }, + Entry("with no configured headers", headersTableInput{ + headers: []options.Header{}, + initialHeaders: http.Header{ + "Foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{}, + expectedHeaders: http.Header{ + "Foo": []string{"bar", "baz"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "Foo": []string{"bar", "baz"}, + "Claim": []string{"IDToken-1234"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header (without preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "Claim": []string{"bar", "baz", "IDToken-1234"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header (with preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + PreserveRequestValue: true, + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + IDToken: "IDToken-1234", + }, + expectedHeaders: http.Header{ + "Claim": []string{"bar", "baz", "IDToken-1234"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header that's not present (without preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: nil, + expectedHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + expectedErr: "", + }), + Entry("with a claim valued header that's not present (with preservation)", headersTableInput{ + headers: []options.Header{ + { + Name: "Claim", + PreserveRequestValue: true, + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "id_token", + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + session: nil, + expectedHeaders: http.Header{ + "Claim": []string{"bar", "baz"}, + }, + expectedErr: "", + }), + Entry("with an invalid basicAuthPassword claim valued header", headersTableInput{ + headers: []options.Header{ + { + Name: "X-Auth-Request-Authorization", + Values: []options.HeaderValue{ + { + ClaimSource: &options.ClaimSource{ + Claim: "user", + BasicAuthPassword: &options.SecretSource{ + Value: []byte(base64.StdEncoding.EncodeToString([]byte("basic-password"))), + FromEnv: "SECRET_ENV", + }, + }, + }, + }, + }, + }, + initialHeaders: http.Header{ + "foo": []string{"bar", "baz"}, + }, + session: &sessionsapi.SessionState{ + User: "user-123", + }, + expectedHeaders: nil, + expectedErr: "error building response header injector: error building response injector: error building injector for header \"X-Auth-Request-Authorization\": error loading basicAuthPassword: secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile", + }), + ) +}) From c9b34228013b844071fd74256f87196aa3d68778 Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Sat, 3 Oct 2020 12:51:59 +0100 Subject: [PATCH 15/17] Add changelog entry for generic header injectors --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3037b42..d61f800f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ ## Changes since v6.1.1 +- [#705](https://github.com/oauth2-proxy/oauth2-proxy/pull/705) Add generic Header injectors for upstream request and response headers (@JoelSpeed) - [#753](https://github.com/oauth2-proxy/oauth2-proxy/pull/753) Pass resource parameter in login url (@codablock) - [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) Add `--skip-auth-route` configuration option for `METHOD=pathRegex` based allowlists (@NickMeves) - [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Stop accepting legacy SHA1 signed cookies (@NickMeves) From 70990327d187920c327cedfb847c0cecf70a0a4f Mon Sep 17 00:00:00 2001 From: Joel Speed Date: Sat, 3 Oct 2020 18:57:25 +0100 Subject: [PATCH 16/17] Make claims list of strings --- pkg/apis/sessions/session_state.go | 27 +++++++++++++------------ pkg/header/injector.go | 32 ++++++++++++++++++------------ 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index 3f675135..03bc747a 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -8,7 +8,6 @@ import ( "io" "io/ioutil" "reflect" - "strings" "time" "unicode/utf8" @@ -70,31 +69,33 @@ func (s *SessionState) String() string { return o + "}" } -func (s *SessionState) GetClaim(claim string) string { +func (s *SessionState) GetClaim(claim string) []string { if s == nil { - return "" + return []string{} } switch claim { case "access_token": - return s.AccessToken + return []string{s.AccessToken} case "id_token": - return s.IDToken + return []string{s.IDToken} case "created_at": - return s.CreatedAt.String() + return []string{s.CreatedAt.String()} case "expires_on": - return s.ExpiresOn.String() + return []string{s.ExpiresOn.String()} case "refresh_token": - return s.RefreshToken + return []string{s.RefreshToken} case "email": - return s.Email + return []string{s.Email} case "user": - return s.User + return []string{s.User} case "groups": - return strings.Join(s.Groups, ",") + groups := make([]string, len(s.Groups)) + copy(groups, s.Groups) + return groups case "preferred_username": - return s.PreferredUsername + return []string{s.PreferredUsername} default: - return "" + return []string{} } } diff --git a/pkg/header/injector.go b/pkg/header/injector.go index 136185e5..9c6e2fcd 100644 --- a/pkg/header/injector.go +++ b/pkg/header/injector.go @@ -85,28 +85,34 @@ func newClaimInjector(name string, source *options.ClaimSource) (valueInjector, return nil, fmt.Errorf("error loading basicAuthPassword: %v", err) } return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { - claim := session.GetClaim(source.Claim) - if claim == "" { - return + claimValues := session.GetClaim(source.Claim) + for _, claim := range claimValues { + if claim == "" { + continue + } + auth := claim + ":" + string(password) + header.Add(name, "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) } - auth := claim + ":" + string(password) - header.Add(name, "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) }), nil case source.Prefix != "": return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { - claim := session.GetClaim(source.Claim) - if claim == "" { - return + claimValues := session.GetClaim(source.Claim) + for _, claim := range claimValues { + if claim == "" { + continue + } + header.Add(name, source.Prefix+claim) } - header.Add(name, source.Prefix+claim) }), nil default: return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { - claim := session.GetClaim(source.Claim) - if claim == "" { - return + claimValues := session.GetClaim(source.Claim) + for _, claim := range claimValues { + if claim == "" { + continue + } + header.Add(name, claim) } - header.Add(name, claim) }), nil } } From f705d2b5d33ef7b18edc0716823454fcb548a55a Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Thu, 8 Oct 2020 02:46:41 +0900 Subject: [PATCH 17/17] Improve CI (#819) * simplify github actions workflow no more GOPATH, update Go to 1.15.x * add script to install golangci-lint * drop support for Go 1.14 * check docker build in ci * update alpine linux to 3.12 * update CHANGELOG * fix golangci-lint installation Co-authored-by: Joel Speed --- .github/workflows/ci.yaml | 28 ++++++++++++++++------------ CHANGELOG.md | 1 + Dockerfile | 5 +---- Dockerfile.arm64 | 5 +---- Dockerfile.armv6 | 5 +---- Makefile | 2 +- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c5e63403..1b0d26c4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,42 +14,46 @@ jobs: build: env: COVER: true - GOPATH: ${{ github.workspace }} runs-on: ubuntu-18.04 steps: - - name: Check out code into the Go module directory + - name: Check out code uses: actions/checkout@v2 - with: - path: ./src/github.com/${{ github.repository }} - - name: Set up Go 1.14 + - name: Set up Go 1.15 uses: actions/setup-go@v2 with: - go-version: ^1.14 + go-version: 1.15.x id: go - name: Get dependencies run: | - cd src/github.com/${{ github.repository }} - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $GOPATH/bin v1.24.0 - GO111MODULE=on go mod download + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 + go mod download curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter chmod +x ./cc-test-reporter - name: Lint run: | - cd src/github.com/${{ github.repository }} make lint - name: Build run: | - cd src/github.com/${{ github.repository }} make build - name: Test env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} run: | - cd src/github.com/${{ github.repository }} ./test.sh + + docker: + runs-on: ubuntu-18.04 + steps: + + - name: Check out code + uses: actions/checkout@v2 + + - name: Docker Build + run: | + make docker diff --git a/CHANGELOG.md b/CHANGELOG.md index d61f800f..62a9c5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - [#801](https://github.com/oauth2-proxy/oauth2-proxy/pull/801) Update go-redis/redis to v8 (@johejo) - [#750](https://github.com/oauth2-proxy/oauth2-proxy/pull/750) ci: Migrate to Github Actions (@shinebayar-g) - [#829](https://github.com/oauth2-proxy/oauth2-proxy/pull/820) Rename test directory to testdata (@johejo) +- [#819](https://github.com/oauth2-proxy/oauth2-proxy/pull/819) Improve CI (@johejo) # v6.1.1 diff --git a/Dockerfile b/Dockerfile index 310eec1f..a47a9ce0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,6 @@ FROM golang:1.15-buster AS builder ARG VERSION -# Download tools -RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 - # Copy sources WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy @@ -23,7 +20,7 @@ COPY . . RUN VERSION=${VERSION} make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM alpine:3.11 +FROM alpine:3.12 COPY nsswitch.conf /etc/nsswitch.conf COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 4f7433f9..9b3b09aa 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,9 +1,6 @@ FROM golang:1.15-buster AS builder ARG VERSION -# Download tools -RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 - # Copy sources WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy @@ -23,7 +20,7 @@ COPY . . RUN VERSION=${VERSION} GOARCH=arm64 make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM arm64v8/alpine:3.11 +FROM arm64v8/alpine:3.12 COPY nsswitch.conf /etc/nsswitch.conf COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy diff --git a/Dockerfile.armv6 b/Dockerfile.armv6 index a69cf318..88bfd58a 100644 --- a/Dockerfile.armv6 +++ b/Dockerfile.armv6 @@ -1,9 +1,6 @@ FROM golang:1.15-buster AS builder ARG VERSION -# Download tools -RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 - # Copy sources WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy @@ -23,7 +20,7 @@ COPY . . RUN VERSION=${VERSION} GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem # Copy binary to alpine -FROM arm32v6/alpine:3.11 +FROM arm32v6/alpine:3.12 COPY nsswitch.conf /etc/nsswitch.conf COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy diff --git a/Makefile b/Makefile index 3b7ea3b4..05c85b86 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ REGISTRY ?= quay.io/oauth2-proxy GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1) GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2) MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1 -MINIMUM_SUPPORTED_GO_MINOR_VERSION = 14 +MINIMUM_SUPPORTED_GO_MINOR_VERSION = 15 GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION).$(MINIMUM_SUPPORTED_GO_MINOR_VERSION) DOCKER_BUILD := docker build --build-arg VERSION=${VERSION}