diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..1b0d26c4 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,59 @@ +name: Continuous Integration + +on: + push: + branches: + - '**' + # - $default-branch + pull_request: + branches: + - '**' + # - $default-branch + +jobs: + build: + env: + COVER: true + runs-on: ubuntu-18.04 + steps: + + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Go 1.15 + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + id: go + + - name: Get dependencies + run: | + 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: | + make lint + + - name: Build + run: | + make build + + - name: Test + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + run: | + ./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/.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..62a9c5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ## Important Notes +- [#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 @@ -22,13 +25,21 @@ ## 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) - [#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) - [#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) +- [#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) +- [#819](https://github.com/oauth2-proxy/oauth2-proxy/pull/819) Improve CI (@johejo) # v6.1.1 diff --git a/Dockerfile b/Dockerfile index a989cb73..a47a9ce0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,6 @@ -FROM golang:1.14-buster AS builder +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 642770fe..9b3b09aa 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,9 +1,6 @@ -FROM golang:1.14-buster AS builder +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 2d16e3e2..88bfd58a 100644 --- a/Dockerfile.armv6 +++ b/Dockerfile.armv6 @@ -1,9 +1,6 @@ -FROM golang:1.14-buster AS builder +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 19b91a18..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} @@ -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: 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). 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/go.mod b/go.mod index 7e4d28bf..fcacdcc1 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 @@ -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/oauthproxy.go b/oauthproxy.go index 092dcc93..e64ffe91 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,53 @@ 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) { + routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) + + for _, path := range opts.SkipAuthRegex { + compiledRegex, err := regexp.Compile(path) + if err != nil { + return nil, err + } + logger.Printf("Skipping auth - Method: ALL | Path: %s", path) + routes = append(routes, allowedRoute{ + method: "", + pathRegex: compiledRegex, + }) + } + + for _, methodPath := range opts.SkipAuthRoutes { + var ( + method string + path string + ) + + parts := strings.SplitN(methodPath, "=", 2) + if len(parts) == 1 { + method = "" + path = parts[0] + } else { + method = strings.ToUpper(parts[0]) + path = parts[1] + } + + compiledRegex, err := regexp.Compile(path) + if err != nil { + return nil, err + } + logger.Printf("Skipping auth - Method: %s | Path: %s", method, path) + 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 +636,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 +695,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 +883,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 +1078,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/oauthproxy_test.go b/oauthproxy_test.go index 33f131cd..d0fd9481 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) } @@ -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,255 @@ 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 + expectedRoutes []expectedAllowedRoute + shouldError bool + }{ + { + name: "No skip auth configured", + skipAuthRegex: []string{}, + skipAuthRoutes: []string{}, + expectedRoutes: []expectedAllowedRoute{}, + shouldError: false, + }, + { + name: "Only skipAuthRegex configured", + skipAuthRegex: []string{ + "^/foo/bar", + "^/baz/[0-9]+/thing", + }, + skipAuthRoutes: []string{}, + expectedRoutes: []expectedAllowedRoute{ + { + method: "", + regexString: "^/foo/bar", + }, + { + method: "", + regexString: "^/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", + }, + 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, + }, + { + 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$", + }, + 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, + }, + { + name: "Invalid skipAuthRegex entry", + skipAuthRegex: []string{ + "^/foo/bar", + "^/baz/[0-9]+/thing", + "(bad[regex", + }, + skipAuthRoutes: []string{}, + expectedRoutes: []expectedAllowedRoute{}, + shouldError: true, + }, + { + name: "Invalid skipAuthRoutes entry", + skipAuthRegex: []string{}, + skipAuthRoutes: []string{ + "GET=^/foo/bar", + "POST=^/baz/[0-9]+/thing", + "^/all/methods$", + "PUT=(bad[regex", + }, + expectedRoutes: []expectedAllowedRoute{}, + 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 + } + assert.NoError(t, err) + + for i, route := range routes { + 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) + } + }) + } +} + +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 +2515,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 +2537,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 { 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 +} 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/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..03bc747a 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -69,6 +69,36 @@ func (s *SessionState) String() string { return o + "}" } +func (s *SessionState) GetClaim(claim string) []string { + if s == nil { + return []string{} + } + switch claim { + case "access_token": + return []string{s.AccessToken} + case "id_token": + return []string{s.IDToken} + case "created_at": + return []string{s.CreatedAt.String()} + case "expires_on": + return []string{s.ExpiresOn.String()} + case "refresh_token": + return []string{s.RefreshToken} + case "email": + return []string{s.Email} + case "user": + return []string{s.User} + case "groups": + groups := make([]string, len(s.Groups)) + copy(groups, s.Groups) + return groups + case "preferred_username": + return []string{s.PreferredUsername} + default: + return []string{} + } +} + // 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..9c6e2fcd --- /dev/null +++ b/pkg/header/injector.go @@ -0,0 +1,118 @@ +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) { + 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))) + } + }), nil + case source.Prefix != "": + return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { + claimValues := session.GetClaim(source.Claim) + for _, claim := range claimValues { + if claim == "" { + continue + } + header.Add(name, source.Prefix+claim) + } + }), nil + default: + return newInjectorFunc(func(header http.Header, session *sessionsapi.SessionState) { + claimValues := session.GetClaim(source.Claim) + for _, claim := range claimValues { + if claim == "" { + continue + } + 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, + }), + ) + }) +}) 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", + }), + ) +}) 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/allowlist.go b/pkg/validation/allowlist.go new file mode 100644 index 00000000..56a3fd4c --- /dev/null +++ b/pkg/validation/allowlist.go @@ -0,0 +1,70 @@ +package validation + +import ( + "fmt" + "os" + "regexp" + "strings" + + "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 { + 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.SplitN(route, "=", 2) + if len(parts) == 1 { + regex = parts[0] + } else { + regex = 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..4600a718 --- /dev/null +++ b/pkg/validation/allowlist_test.go @@ -0,0 +1,125 @@ +package validation + +import ( + . "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() { + type validateRoutesTableInput struct { + routes []string + errStrings []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$", + }, + errStrings: []string{}, + }), + Entry("Bad regexes do not compile", &validateRoutesTableInput{ + routes: []string{ + "POST=/(foo", + "OPTIONS=/foo/bar)", + "GET=^]/foo/bar[$", + "GET=^]/foo/bar[$", + }, + 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 ]: `[$`", + }, + }), + ) + + DescribeTable("validateRegexes", + func(r *validateRegexesTableInput) { + opts := &options.Options{ + SkipAuthRegex: r.regexes, + } + Expect(validateRegexes(opts)).To(ConsistOf(r.errStrings)) + }, + Entry("Valid regex routes", &validateRegexesTableInput{ + regexes: []string{ + "/foo", + "/foo/bar", + "^/foo/bar$", + "/crazy/(?:regex)?/[^/]+/stuff$", + }, + errStrings: []string{}, + }), + Entry("Bad regexes do not compile", &validateRegexesTableInput{ + regexes: []string{ + "/(foo", + "/foo/bar)", + "^]/foo/bar[$", + "^]/foo/bar[$", + }, + 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 ]: `[$`", + }, + }), + ) + + DescribeTable("validateTrustedIPs", + func(t *validateTrustedIPsTableInput) { + opts := &options.Options{ + TrustedIPs: t.trustedIPs, + } + 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", + }, + 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", + }, + 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", + }, + }), + ) +}) 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) 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" 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 diff --git a/test/openredirects.txt b/testdata/openredirects.txt similarity index 100% rename from test/openredirects.txt rename to testdata/openredirects.txt