diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..7db07fc7 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '32 3 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..80f84b31 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,85 @@ +name: docker-nightly + +on: + push: + branches: + - main + tags: + - '*.*.*' + pull_request: + +jobs: + + docker: + name: Docker + runs-on: ubuntu-latest + + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.24.x" + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Build Data + id: info + run: | + echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') + export TEMP=$(cd auth_server && go run gen_version.go) + echo ::set-output name=version::$(echo -n $TEMP | awk '{print $1}') + echo ::set-output name=build_id::$(echo -n $TEMP | awk '{print $2}') + + - name: Docker meta + id: docker_meta + uses: crazy-max/ghaction-docker-meta@v5 + with: + images: cesanta/docker_auth + tag-edge: true + tag-semver: | + {{version}} + {{major}} + {{major}}.{{minor}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + version: latest + # TODO: Remove driver-opts once fix is released docker/buildx#386 + driver-opts: image=moby/buildkit:master + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + if: github.event_name == 'push' + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + context: auth_server + file: auth_server/Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.docker_meta.outputs.tags }} + build-args: | + VERSION=${{ steps.info.outputs.version }} + BUILD_ID=${{ steps.info.outputs.build_id }} + labels: | + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.description=${{ github.event.repository.description }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.source=${{ github.event.repository.clone_url }} + org.opencontainers.image.version=${{ steps.imagetag.outputs.value }} + org.opencontainers.image.created=${{ steps.info.outputs.created }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.licenses=${{ github.event.repository.license.spdx_id }} diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml new file mode 100644 index 00000000..50c4821b --- /dev/null +++ b/.github/workflows/go_test.yml @@ -0,0 +1,24 @@ +on: [push, pull_request] +name: Test +jobs: + test: + strategy: + matrix: + go-version: [1.23.x,1.24.x] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v4 + - name: Test + run: | + cd auth_server + go test ./... + - name: Build + run: | + cd auth_server + make diff --git a/.gitignore b/.gitignore index 1377554e..5aaadfcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.swp +chart/docker-auth/Chart.lock diff --git a/README.md b/README.md index c36b6002..5e00a657 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Docker Registry 2.0 authentication server +Docker Registry 2 authentication server ========================================= The original Docker Registry server (v1) did not provide any support for authentication or authorization. @@ -8,18 +8,42 @@ While performing simple user authentication is pretty straightforward, performin Docker Registry 2.0 introduced a new, token-based authentication and authorization protocol, but the server to generate them was not released. Thus, most guides found on the internet still describe a set up with a reverse proxy performing access control. -This server fills the gap and implements the protocol described [here](https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md). +This server fills the gap and implements the protocol described [here](https://github.com/docker/distribution/blob/main/docs/spec/auth/token.md). Supported authentication methods: * Static list of users - * Google Sign-In (incl. Google for Work / GApps for domain) (documented [here](https://github.com/cesanta/docker_auth/blob/master/examples/reference.yml)) - * LDAP bind + * Google Sign-In (incl. Google for Work / GApps for domain) (documented [here](https://github.com/cesanta/docker_auth/blob/main/examples/reference.yml)) + * [Github Sign-In](docs/auth-methods.md#github) + * Gitlab Sign-In + * LDAP bind ([demo](https://github.com/kwk/docker-registry-setup)) + * MongoDB user collection + * MySQL/MariaDB, PostgreSQL, SQLite database table + * [External program](https://github.com/cesanta/docker_auth/blob/main/examples/ext_auth.sh) + +Supported authorization methods: + * Static ACL + * MongoDB-backed ACL + * MySQL/MariaDB, PostgreSQL, SQLite backed ACL + * External program ## Installation and Examples -A public Docker image is available on Docker Hub: [cesanta/docker_auth:stable](https://registry.hub.docker.com/u/cesanta/docker_auth/). +### Using Helm/Kubernetes + +A helm chart is available in the folder [chart/docker-auth](chart/docker-auth). + +### Docker + +A public Docker image is available on Docker Hub: [cesanta/docker_auth](https://hub.docker.com/r/cesanta/docker_auth/). + +Tags available: + - `:edge` - bleeding edge, usually works but breaking config changes are possible. You probably do not want to use this in production. + - `:latest` - latest tagged release, will line up with `:1` tag + - `:1` - the `1.x` version, will have fixes, no breaking config changes. Previously known as `:stable`. + - `:1.x` - specific release, see [here](https://github.com/cesanta/docker_auth/releases) for the list of current releases. The binary takes a single argument - path to the config file. +If no arguments are given, the Dockerfile defaults to `/config/auth_config.yml`. Example command line: @@ -28,16 +52,16 @@ $ docker run \ --rm -it --name docker_auth -p 5001:5001 \ -v /path/to/config_dir:/config:ro \ -v /var/log/docker_auth:/logs \ - cesanta/docker_auth /config/auth_config.yml + cesanta/docker_auth:1 /config/auth_config.yml ``` -See the [example config files](https://github.com/cesanta/docker_auth/tree/master/examples/) to get an idea of what is possible. +See the [example config files](https://github.com/cesanta/docker_auth/tree/main/examples/) to get an idea of what is possible. ## Troubleshooting Run with increased verbosity: ```{r, engine='bash', count_lines} -docker run ... cesanta/docker_auth --v=2 --alsologtostderr /config/auth_config.yml +docker run ... cesanta/docker_auth:1 --v=2 --alsologtostderr /config/auth_config.yml ``` ## Contributing diff --git a/auth_server/.gitignore b/auth_server/.gitignore index 24443656..e63dae2a 100644 --- a/auth_server/.gitignore +++ b/auth_server/.gitignore @@ -1 +1,4 @@ +ca-certificates.crt auth_server +vendor/*/ +version.* diff --git a/auth_server/Dockerfile b/auth_server/Dockerfile index 2e89ef53..c489ad6e 100644 --- a/auth_server/Dockerfile +++ b/auth_server/Dockerfile @@ -1,4 +1,20 @@ -FROM golang:1.5 +FROM golang:1.24-alpine3.22 AS build + +ARG VERSION +ENV VERSION="${VERSION}" +ARG BUILD_ID +ENV BUILD_ID="${BUILD_ID}" +ARG CGO_EXTRA_CFLAGS + +RUN apk add -U --no-cache ca-certificates make git gcc musl-dev binutils-gold + +COPY . /build +WORKDIR /build +RUN make build + +FROM alpine:3.22 +COPY --from=build /build/auth_server /docker_auth/ +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +ENTRYPOINT ["/docker_auth/auth_server"] +CMD ["/config/auth_config.yml"] EXPOSE 5001 -ADD auth_server / -ENTRYPOINT ["/auth_server"] diff --git a/auth_server/Makefile b/auth_server/Makefile index c8e856d7..120d1a89 100644 --- a/auth_server/Makefile +++ b/auth_server/Makefile @@ -1,20 +1,34 @@ -.PHONY: update-deps build docker-build +MAKEFLAGS += --warn-undefined-variables +IMAGE ?= cesanta/docker_auth +VERSION ?= $(shell go run ./gen_version.go | awk '{print $$1}') +BUILD_ID ?= $(shell go run ./gen_version.go | awk '{print $$2}') -all: build +.PHONY: % -update-deps: - go get -v -u -f github.com/jteeuwen/go-bindata/... . +all: build build: - go generate ./... - go build + go build -v -ldflags="-extldflags '-static' -X 'main.Version=${VERSION}' -X 'main.BuildID=${BUILD_ID}'" + +auth_server: + @echo + @echo Use build or build-release to produce the auth_server binary + @echo + @exit 1 + +docker-build: + docker build --build-arg VERSION="${VERSION}" --build-arg BUILD_ID="${BUILD_ID}" -t $(IMAGE):latest . + docker tag $(IMAGE):latest $(IMAGE):$(VERSION) + +docker-tag-%: + docker tag $(IMAGE):latest $(IMAGE):$* -docker-build: update-deps build - docker build -t cesanta/docker_auth -f Dockerfile . +docker-push: + docker push $(IMAGE):latest + docker push $(IMAGE):$(VERSION) -docker-push-latest: - docker push cesanta/docker_auth:latest +docker-push-%: docker-tag-% + docker push $(IMAGE):$* -docker-push-stable: - docker tag -f cesanta/docker_auth:latest cesanta/docker_auth:stable - docker push cesanta/docker_auth:stable +clean: + rm -rf auth_server vendor/*/* diff --git a/auth_server/README.md b/auth_server/README.md new file mode 100644 index 00000000..00f30fe5 --- /dev/null +++ b/auth_server/README.md @@ -0,0 +1,9 @@ +### Building local image + +``` +mkdir -p /var/tmp/go/src/github.com/cesanta +cd /var/tmp/go/src/github.com/cesanta +git clone https://github.com/cesanta/docker_auth.git +cd docker_auth/auth_server +make docker-build +``` diff --git a/auth_server/api/authn.go b/auth_server/api/authn.go new file mode 100644 index 00000000..8cd132f8 --- /dev/null +++ b/auth_server/api/authn.go @@ -0,0 +1,52 @@ +/* + Copyright 2019 Cesanta Software Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package api + +import "errors" + +type Labels map[string][]string + +// Authentication plugin interface. +type Authenticator interface { + // Given a user name and a password (plain text), responds with the result or an error. + // Error should only be reported if request could not be serviced, not if it should be denied. + // A special NoMatch error is returned if the authorizer could not reach a decision, + // e.g. none of the rules matched. + // Another special WrongPass error is returned if the authorizer failed to authenticate. + // Implementations must be goroutine-safe. + Authenticate(user string, password PasswordString) (bool, Labels, error) + + // Finalize resources in preparation for shutdown. + // When this call is made there are guaranteed to be no Authenticate requests in flight + // and there will be no more calls made to this instance. + Stop() + + // Human-readable name of the authenticator. + Name() string +} + +var NoMatch = errors.New("did not match any rule") +var WrongPass = errors.New("wrong password for user") + +type PasswordString string + +func (ps PasswordString) String() string { + if len(ps) == 0 { + return "" + } + return "***" +} diff --git a/auth_server/authz/authz.go b/auth_server/api/authz.go similarity index 68% rename from auth_server/authz/authz.go rename to auth_server/api/authz.go index 9b702124..6d03ead8 100644 --- a/auth_server/authz/authz.go +++ b/auth_server/api/authz.go @@ -1,8 +1,24 @@ -package authz +/* + Copyright 2019 Cesanta Software Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package api import ( - "errors" "fmt" + "net" "strings" ) @@ -29,14 +45,14 @@ type Authorizer interface { Name() string } -var NoMatch = errors.New("did not match any rule") - type AuthRequestInfo struct { Account string Type string Name string Service string + IP net.IP Actions []string + Labels Labels } func (ai AuthRequestInfo) String() string { diff --git a/auth_server/authn/authn.go b/auth_server/authn/authn.go index 5f5a8b56..a3ab2461 100644 --- a/auth_server/authn/authn.go +++ b/auth_server/authn/authn.go @@ -1,5 +1,5 @@ /* - Copyright 2015 Cesanta Software Ltd. + Copyright 2020 Cesanta Software Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,35 +16,7 @@ package authn -import "errors" +import "embed" -// Authentication plugin interface. -type Authenticator interface { - // Given a user name and a password (plain text), responds with the result or an error. - // Error should only be reported if request could not be serviced, not if it should be denied. - // A special NoMatch error is returned if the authorizer could not reach a decision, - // e.g. none of the rules matched. - // Implementations must be goroutine-safe. - Authenticate(user string, password PasswordString) (bool, error) - - // Finalize resources in preparation for shutdown. - // When this call is made there are guaranteed to be no Authenticate requests in flight - // and there will be no more calls made to this instance. - Stop() - - // Human-readable name of the authenticator. - Name() string -} - -var NoMatch = errors.New("did not match any rule") - -//go:generate go-bindata -pkg authn -modtime 1 -mode 420 data/ - -type PasswordString string - -func (ps PasswordString) String() string { - if len(ps) == 0 { - return "" - } - return "***" -} +//go:embed data/* +var static embed.FS diff --git a/auth_server/authn/bindata.go b/auth_server/authn/bindata.go deleted file mode 100644 index ec2adb3d..00000000 --- a/auth_server/authn/bindata.go +++ /dev/null @@ -1,238 +0,0 @@ -// Code generated by go-bindata. -// sources: -// data/google_auth.tmpl -// DO NOT EDIT! - -package authn - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "strings" - "os" - "time" - "io/ioutil" - "path/filepath" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -type asset struct { - bytes []byte - info os.FileInfo -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _dataGoogle_authTmpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xdc\x56\x6d\x6b\xeb\x36\x14\xfe\x9c\xfc\x0a\xe1\x5d\xb0\xc3\x7a\x65\x76\x3f\x8c\x4b\x6e\x92\xd1\xdd\xc1\xe8\x18\x6b\x59\xbb\x4f\x63\x04\x55\x3e\x76\xd4\x2a\x92\x27\x1d\x77\x0d\x21\xff\x7d\x47\x92\xf3\xd2\x97\x84\x0e\xc6\xc6\x16\x68\x63\x9f\xa3\xf3\xf6\xe8\x79\x22\x4d\x16\xb8\xd4\x4c\x21\x2c\xbd\xb4\x2d\xc4\x27\x5c\xb5\x30\xcd\x16\x88\xed\xb8\x2c\xbd\x5c\xc0\x52\x70\xeb\x9a\xf2\xdc\xa1\x92\x1a\xb2\xd9\x70\xb2\x00\x51\xcd\x86\x8c\x4d\xbc\x74\xaa\x45\xe6\x9d\x9c\x66\x65\x29\xee\xc4\x23\x6f\xac\x6d\x34\x88\x56\x79\x2e\xed\x32\xda\x4a\xad\x6e\x7d\x79\xf7\x7b\x07\x6e\x55\x7e\xc5\x3f\xf2\x0f\xfd\x0b\x5f\x2a\xc3\xef\x7c\x36\x9b\x94\x29\xd3\x8b\xa4\xa1\x0d\x4f\x7d\xc4\x7c\x29\x75\x4c\x7b\xe7\x4b\xa9\x15\x18\x1c\xb7\x5a\x60\x6d\xdd\x92\xf2\x7c\x63\x8d\xb6\xa2\x9a\x7a\x14\x0e\x33\x26\xfc\xca\x48\x56\x41\x0d\xee\xb5\x02\xb3\xe1\x60\x50\x77\x46\xa2\xb2\x86\xd1\x98\xf2\xfe\x47\xdb\x28\x53\x8c\xd8\x9a\x3c\x83\x07\xe1\x98\xe8\x70\xf1\x81\x4d\x59\x43\xe5\x79\x7c\xe1\x0d\xe0\x39\x3d\x5c\x18\x2a\x62\x24\x14\xa3\x4f\x61\xb1\xaa\x59\x91\xfc\xca\x5f\xab\xc6\x40\x75\x61\xc2\xd2\x62\xd4\x67\x1b\xbc\x2b\xf2\x2f\x1c\xf8\x4e\x63\x3e\xe2\x08\x8f\x58\xe4\x0f\x42\xab\x4a\xa0\x32\x0d\x83\x47\xe5\xe3\x03\xda\x7b\x30\x9c\xf3\x3c\xe5\x8d\x5d\xa8\x6a\x1e\xcd\xd4\x48\xaa\x21\x3b\xe7\x68\xf4\x5f\x3c\xb8\x54\x64\xdb\xd5\xcf\xe0\x5b\x6b\x3c\x75\xc5\xb7\x41\x29\xcd\x3b\x1e\xf6\xa1\x48\xad\x0c\xc2\x0e\x8f\x59\x7e\x75\x79\x7d\x93\x9f\x25\x53\xe7\x34\x59\xca\x84\xf0\x3c\x94\xd9\x7a\xa4\x35\x48\xc5\x6e\x52\x8c\x68\x5b\xad\xa4\x08\x98\xd1\x1e\x58\xf3\x89\x90\x13\xce\x03\x4e\x3b\xac\xdf\x7f\xdc\x06\xb5\xce\x4a\xf0\xfe\x3b\x81\x62\xcc\x6a\xa1\x3d\xf4\x8e\x2a\x5a\x7e\xb8\xbe\xfc\x89\x7b\x74\x34\xb1\xaa\x57\xc5\x3a\x17\x71\x17\x72\x2a\x10\x37\x22\x3f\x63\x79\xec\x9e\x2c\xdb\x41\x36\xa3\x3e\x85\xef\x64\xc8\x4d\x79\xfb\xcd\x2b\x12\xae\x5b\xa4\x5f\xc3\xba\x5f\x91\xc0\x18\x6c\xfa\x4c\xe0\x9c\x75\x07\x79\x1e\x17\xee\x54\x92\xbc\x5f\x9f\xb3\x2f\x19\x2d\xe5\xae\x47\xfb\x86\x9c\xcf\x52\x6f\xd2\xfb\x86\x01\x8d\xde\xa7\x24\x20\xbd\x25\xfa\x6a\xdb\x14\xb9\xb1\xc8\xe8\xa1\x81\x8a\x29\xd3\x6f\xf6\x66\x18\xfe\x18\x7d\x76\xb4\x8c\x4c\x8e\x8c\x64\xf1\x13\x89\x18\x38\x5e\xe4\x91\x0a\x04\xd4\xae\xfb\x6d\xeb\x07\x64\x55\x46\x61\xb1\x4e\x42\x99\xab\x8a\x5a\x5f\xaf\xf9\xe7\xf8\x7a\x51\x6d\x36\xf9\x86\x26\x5b\x80\x29\xf6\xec\xef\x3b\xa1\xaf\x50\x2e\x74\xb3\x57\xce\xa4\x4c\xba\x9f\xdc\xda\x6a\x45\xea\x99\xdc\x76\x88\xd4\xa4\xaa\xa6\x99\x27\xd2\x2b\xf3\x6d\x34\x64\xb3\x20\x01\x9a\x8b\xfd\xa1\x70\xc1\xbe\x8f\x9c\x9a\x94\x69\x75\x88\xdb\xeb\x2f\x80\x7c\x18\x4a\x50\x53\xb7\xf2\xbe\x78\x3e\x55\x59\xb2\xb0\xee\xc2\x7c\x16\x5a\xdf\x0a\x79\x1f\x74\xad\x4c\x84\x8f\x50\x82\x96\x7d\xcd\xff\xb2\x6a\x7b\x9f\x13\x06\x2f\xeb\x5a\x53\xba\xf3\x48\x2d\x22\xa4\x83\x4a\x39\x90\x38\xef\x9c\x0a\xb4\x6c\xad\xc7\x25\xb9\x44\x03\x3b\xd4\x76\x3d\x8a\x24\xbd\x03\x0e\x1e\xee\xf5\x81\xf7\xbf\x26\xc6\x00\xf9\x9c\xf8\x49\x72\x94\xb6\x02\x32\xed\x87\xf9\x35\x99\x7e\xfb\x1b\x75\xf9\x04\xb6\x2c\xf9\xc6\xd9\x19\xfb\xc7\xd4\xfb\x54\xa3\x29\x80\x86\x7f\xa3\xd8\xe3\x57\xfc\xbf\xd7\xcc\x0b\x91\x5c\x76\xf8\x44\x25\xb6\xc3\xd3\xd2\xd8\x05\x1c\xd5\xc6\xff\xea\xa4\x22\xa1\x5f\x81\x0b\x07\x3a\xa3\xa0\x07\x70\xef\xbd\xaa\x20\x8a\x3f\x60\xc5\xff\x15\x05\xbd\x4d\x28\xd4\xde\xc9\x83\xeb\xa8\x0e\x5f\x2a\x87\xf6\xe2\x18\xcd\x0f\x5c\x74\x11\x6a\x35\x20\x1c\x13\xc1\x13\x36\x6f\x11\xec\xd5\x74\x8a\xd7\x83\x53\x2c\xe2\x95\xa2\xbb\xa2\x31\xf4\xe3\x58\xec\x02\x5e\xe1\x8c\x8f\xe4\x0a\x15\xf3\x37\x1c\x8e\xaf\x24\x38\x7a\x44\x3e\x57\x58\xa5\x1e\xa2\xbc\x52\x7c\xb8\x4c\x92\x25\x1c\x57\xe9\x9c\xa2\x63\x8b\x2e\xb9\xb3\xe1\x9f\x01\x00\x00\xff\xff\xf6\x19\xb6\xcf\xec\x0a\x00\x00") - -func dataGoogle_authTmplBytes() ([]byte, error) { - return bindataRead( - _dataGoogle_authTmpl, - "data/google_auth.tmpl", - ) -} - -func dataGoogle_authTmpl() (*asset, error) { - bytes, err := dataGoogle_authTmplBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "data/google_auth.tmpl", size: 2796, mode: os.FileMode(420), modTime: time.Unix(1, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if (err != nil) { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "data/google_auth.tmpl": dataGoogle_authTmpl, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - cannonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(cannonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} -var _bintree = &bintree{nil, map[string]*bintree{ - "data": &bintree{nil, map[string]*bintree{ - "google_auth.tmpl": &bintree{dataGoogle_authTmpl, map[string]*bintree{ - }}, - }}, -}} - -// RestoreAsset restores an asset under the given directory -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) - if err != nil { - return err - } - return nil -} - -// RestoreAssets restores an asset under the given directory recursively -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - cannonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} - diff --git a/auth_server/authn/data/github_auth.tmpl b/auth_server/authn/data/github_auth.tmpl new file mode 100644 index 00000000..4ec1afc6 --- /dev/null +++ b/auth_server/authn/data/github_auth.tmpl @@ -0,0 +1,75 @@ + + + +
+ +
+
+
+ Login{{if .Organization}} to @{{.Organization}}{{end}} with GitHub
+
+
+ Revoke access +
+$ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ podman login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ nerdctl login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+
+
diff --git a/auth_server/authn/data/gitlab_auth.tmpl b/auth_server/authn/data/gitlab_auth.tmpl
new file mode 100755
index 00000000..8ead6163
--- /dev/null
+++ b/auth_server/authn/data/gitlab_auth.tmpl
@@ -0,0 +1,45 @@
+
+
+
+
+
+ $ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ podman login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ nerdctl login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+
+
diff --git a/auth_server/authn/data/google_auth.tmpl b/auth_server/authn/data/google_auth.tmpl
index b4aee197..0607e5b5 100644
--- a/auth_server/authn/data/google_auth.tmpl
+++ b/auth_server/authn/data/google_auth.tmpl
@@ -38,7 +38,7 @@
$('#signinButton').click(function() {
// signInCallback defined in step 6.
var auth2 = gapi.auth2.getAuthInstance();
- auth2.grantOfflineAccess({'redirect_uri': 'postmessage'}).then(function(authResult) {
+ auth2.grantOfflineAccess({'redirect_uri': 'postmessage', 'prompt': 'consent'}).then(function(authResult) {
console.log(authResult);
$.ajax({
type: 'POST',
diff --git a/auth_server/authn/data/oidc_auth.tmpl b/auth_server/authn/data/oidc_auth.tmpl
new file mode 100644
index 00000000..262c78f4
--- /dev/null
+++ b/auth_server/authn/data/oidc_auth.tmpl
@@ -0,0 +1,18 @@
+
+
+
+
+
+ $ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ podman login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ nerdctl login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+
+
diff --git a/auth_server/authn/ext_auth.go b/auth_server/authn/ext_auth.go
new file mode 100644
index 00000000..7c6757cc
--- /dev/null
+++ b/auth_server/authn/ext_auth.go
@@ -0,0 +1,107 @@
+/*
+ Copyright 2016 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strings"
+ "syscall"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type ExtAuthConfig struct {
+ Command string `yaml:"command"`
+ Args []string `yaml:"args"`
+}
+
+type ExtAuthStatus int
+
+const (
+ ExtAuthAllowed ExtAuthStatus = 0
+ ExtAuthDenied ExtAuthStatus = 1
+ ExtAuthNoMatch ExtAuthStatus = 2
+ ExtAuthError ExtAuthStatus = 3
+)
+
+type ExtAuthResponse struct {
+ Labels api.Labels `json:"labels,omitempty"`
+}
+
+func (c *ExtAuthConfig) Validate() error {
+ if c.Command == "" {
+ return fmt.Errorf("command is not set")
+ }
+ if _, err := exec.LookPath(c.Command); err != nil {
+ return fmt.Errorf("invalid command %q: %s", c.Command, err)
+ }
+ return nil
+}
+
+type extAuth struct {
+ cfg *ExtAuthConfig
+}
+
+func NewExtAuth(cfg *ExtAuthConfig) *extAuth {
+ glog.Infof("External authenticator: %s %s", cfg.Command, strings.Join(cfg.Args, " "))
+ return &extAuth{cfg: cfg}
+}
+
+func (ea *extAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ cmd := exec.Command(ea.cfg.Command, ea.cfg.Args...)
+ cmd.Stdin = strings.NewReader(fmt.Sprintf("%s %s", user, string(password)))
+ output, err := cmd.Output()
+ es := 0
+ et := ""
+ if err == nil {
+ } else if ee, ok := err.(*exec.ExitError); ok {
+ es = ee.Sys().(syscall.WaitStatus).ExitStatus()
+ et = string(ee.Stderr)
+ } else {
+ es = int(ExtAuthError)
+ et = fmt.Sprintf("cmd run error: %s", err)
+ }
+ glog.V(2).Infof("%s %s -> %d %s", cmd.Path, cmd.Args, es, output)
+ switch ExtAuthStatus(es) {
+ case ExtAuthAllowed:
+ var resp ExtAuthResponse
+ if len(output) > 0 {
+ if err = json.Unmarshal(output, &resp); err != nil {
+ return false, nil, err
+ }
+ }
+ return true, resp.Labels, nil
+ case ExtAuthDenied:
+ return false, nil, nil
+ case ExtAuthNoMatch:
+ return false, nil, api.NoMatch
+ default:
+ glog.Errorf("Ext command error: %d %s", es, et)
+ }
+ return false, nil, fmt.Errorf("bad return code from command: %d", es)
+}
+
+func (sua *extAuth) Stop() {
+}
+
+func (sua *extAuth) Name() string {
+ return "external"
+}
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
new file mode 100644
index 00000000..83b8c972
--- /dev/null
+++ b/auth_server/authn/github_auth.go
@@ -0,0 +1,501 @@
+/*
+ Copyright 2016 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type GitHubTeamCollection []GitHubTeam
+
+type GitHubTeam struct {
+ Id int64 `json:"id"`
+ Url string `json:"url,omitempty"`
+ Name string `json:"name,omitempty"`
+ Slug string `json:"slug,omitempty"`
+ Organization *GitHubOrganization `json:"organization"`
+ Parent *ParentGitHubTeam `json:"parent,omitempty"`
+}
+
+type GitHubOrganization struct {
+ Login string `json:"login"`
+ Id int64 `json:"id,omitempty"`
+}
+
+type ParentGitHubTeam struct {
+ Id int64 `json:"id"`
+ Name string `json:"name,omitempty"`
+ Slug string `json:"slug,omitempty"`
+}
+
+type GitHubAuthConfig struct {
+ Organization string `yaml:"organization,omitempty"`
+ ClientId string `yaml:"client_id,omitempty"`
+ ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ LevelTokenDB *LevelDBStoreConfig `yaml:"level_token_db,omitempty"`
+ GCSTokenDB *GCSStoreConfig `yaml:"gcs_token_db,omitempty"`
+ RedisTokenDB *RedisStoreConfig `yaml:"redis_token_db,omitempty"`
+ HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
+ RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
+ GithubWebUri string `yaml:"github_web_uri,omitempty"`
+ GithubApiUri string `yaml:"github_api_uri,omitempty"`
+ RegistryUrl string `yaml:"registry_url,omitempty"`
+}
+
+type GitHubAuthRequest struct {
+ Action string `json:"action,omitempty"`
+ Code string `json:"code,omitempty"`
+ Token string `json:"token,omitempty"`
+}
+
+type GitHubTokenUser struct {
+ Login string `json:"login,omitempty"`
+ Email string `json:"email,omitempty"`
+}
+
+type GitHubAuth struct {
+ config *GitHubAuthConfig
+ db TokenDB
+ client *http.Client
+ tmpl *template.Template
+ tmplResult *template.Template
+}
+
+type linkHeader struct {
+ First string
+ Last string
+ Next string
+ Prev string
+}
+
+func execGHExperimentalApiRequest(url string, token string) (*http.Response, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ err = fmt.Errorf("could not create an http request for uri: %s. Error: %s", url, err)
+ return nil, err
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
+ // Currently an "experimental" API; https://developer.github.com/v3/orgs/teams/#list-user-teams
+ req.Header.Add("Accept", "application/vnd.github.hellcat-preview+json")
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ err = fmt.Errorf("HTTP error while retrieving %s. Error : %s", url, err)
+ return nil, err
+ }
+
+ return resp, nil
+}
+
+// removeSubstringsFromString removes all occurences of stringsToStrip from sourceStr
+func removeSubstringsFromString(sourceStr string, stringsToStrip []string) string {
+ theNewString := sourceStr
+ for _, i := range stringsToStrip {
+ theNewString = strings.Replace(theNewString, i, "", -1)
+ }
+ return theNewString
+}
+
+// parseLinkHeader parses the HTTP headers from the Github API response
+//
+// https://developer.github.com/v3/guides/traversing-with-pagination/
+func parseLinkHeader(linkLines []string) (linkHeader, error) {
+ var lH linkHeader
+ // URL in link is enclosed in < >
+ stringsToRemove := []string{"<", ">"}
+
+ for _, linkLine := range linkLines {
+ for _, linkItem := range strings.Split(linkLine, ",") {
+ linkData := strings.Split(linkItem, ";")
+ trimmedUrl := removeSubstringsFromString(strings.TrimSpace(linkData[0]), stringsToRemove)
+ linkVal := linkData[1]
+ switch {
+ case strings.Contains(linkVal, "first"):
+ lH.First = trimmedUrl
+ case strings.Contains(linkVal, "last"):
+ lH.Last = trimmedUrl
+ case strings.Contains(linkVal, "next"):
+ lH.Next = trimmedUrl
+ case strings.Contains(linkVal, "prev"):
+ lH.Prev = trimmedUrl
+ }
+ }
+ }
+ return lH, nil
+}
+
+func NewGitHubAuth(c *GitHubAuthConfig) (*GitHubAuth, error) {
+ var db TokenDB
+ var err error
+ var dbName string
+
+ switch {
+ case c.GCSTokenDB != nil:
+ db, err = NewGCSTokenDB(c.GCSTokenDB)
+ dbName = "GCS: " + c.GCSTokenDB.Bucket
+ case c.RedisTokenDB != nil:
+ db, err = NewRedisTokenDB(c.RedisTokenDB)
+ dbName = db.(*redisTokenDB).String()
+ default:
+ db, err = NewTokenDB(c.LevelTokenDB)
+ dbName = c.LevelTokenDB.Path
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ glog.Infof("GitHub auth token DB at %s", dbName)
+ github_auth, _ := static.ReadFile("data/github_auth.tmpl")
+ github_auth_result, _ := static.ReadFile("data/github_auth_result.tmpl")
+ return &GitHubAuth{
+ config: c,
+ db: db,
+ client: &http.Client{Timeout: c.HTTPTimeout},
+ tmpl: template.Must(template.New("github_auth").Parse(string(github_auth))),
+ tmplResult: template.Must(template.New("github_auth_result").Parse(string(github_auth_result))),
+ }, nil
+}
+
+func (gha *GitHubAuth) doGitHubAuthPage(rw http.ResponseWriter, req *http.Request) {
+ if err := gha.tmpl.Execute(rw, struct {
+ ClientId, GithubWebUri, Organization string
+ }{
+ ClientId: gha.config.ClientId,
+ GithubWebUri: gha.getGithubWebUri(),
+ Organization: gha.config.Organization}); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+func (gha *GitHubAuth) doGitHubAuthResultPage(rw http.ResponseWriter, username string, password string) {
+ if err := gha.tmplResult.Execute(rw, struct {
+ Organization, Username, Password, RegistryUrl string
+ }{Organization: gha.config.Organization,
+ Username: username,
+ Password: password,
+ RegistryUrl: gha.config.RegistryUrl}); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+func (gha *GitHubAuth) DoGitHubAuth(rw http.ResponseWriter, req *http.Request) {
+ code := req.URL.Query().Get("code")
+
+ if code != "" {
+ gha.doGitHubAuthCreateToken(rw, code)
+ } else if req.Method == "GET" {
+ gha.doGitHubAuthPage(rw, req)
+ return
+ }
+}
+
+func (gha *GitHubAuth) getGithubApiUri() string {
+ if gha.config.GithubApiUri != "" {
+ return gha.config.GithubApiUri
+ } else {
+ return "https://api.github.com/"
+ }
+}
+
+func (gha *GitHubAuth) getGithubWebUri() string {
+ if gha.config.GithubWebUri != "" {
+ return gha.config.GithubWebUri
+ } else {
+ return "https://github.com/"
+ }
+}
+
+func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code string) {
+ data := url.Values{
+ "code": []string{string(code)},
+ "client_id": []string{gha.config.ClientId},
+ "client_secret": []string{gha.config.ClientSecret},
+ }
+
+ req, err := http.NewRequest("POST", fmt.Sprintf("%s/login/oauth/access_token", gha.getGithubWebUri()), bytes.NewBufferString(data.Encode()))
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error creating request to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
+ return
+ }
+ req.Header.Add("Accept", "application/json")
+
+ resp, err := gha.client.Do(req)
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error talking to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
+ return
+ }
+ codeResp, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ glog.V(2).Infof("Code to token resp: %s", strings.Replace(string(codeResp), "\n", " ", -1))
+
+ var c2t CodeToTokenResponse
+ err = json.Unmarshal(codeResp, &c2t)
+ if err != nil || c2t.Error != "" || c2t.ErrorDescription != "" {
+ var et string
+ if err != nil {
+ et = err.Error()
+ } else {
+ et = fmt.Sprintf("%s: %s", c2t.Error, c2t.ErrorDescription)
+ }
+ http.Error(rw, fmt.Sprintf("Failed to get token: %s", et), http.StatusBadRequest)
+ return
+ }
+
+ user, err := gha.validateAccessToken(c2t.AccessToken)
+ if err != nil {
+ glog.Errorf("Newly-acquired token is invalid: %+v %s", c2t, err)
+ http.Error(rw, "Newly-acquired token is invalid", http.StatusInternalServerError)
+ return
+ }
+
+ glog.Infof("New GitHub auth token for %s", user)
+
+ userTeams, err := gha.fetchTeams(c2t.AccessToken)
+ if err != nil {
+ glog.Errorf("could not fetch user teams: %s", err)
+ }
+
+ v := &TokenDBValue{
+ TokenType: c2t.TokenType,
+ AccessToken: c2t.AccessToken,
+ ValidUntil: time.Now().Add(gha.config.RevalidateAfter),
+ Labels: map[string][]string{"teams": userTeams},
+ }
+ dp, err := gha.db.StoreToken(user, v, true)
+ if err != nil {
+ glog.Errorf("Failed to record server token: %s", err)
+ http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
+ return
+ }
+
+ gha.doGitHubAuthResultPage(rw, user, dp)
+}
+
+func (gha *GitHubAuth) validateAccessToken(token string) (user string, err error) {
+ glog.Infof("Github API: Fetching user info")
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/user", gha.getGithubApiUri()), nil)
+ if err != nil {
+ err = fmt.Errorf("could not create request to get information for token %s: %s", token, err)
+ return
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
+ req.Header.Add("Accept", "application/json")
+
+ resp, err := gha.client.Do(req)
+ if err != nil {
+ err = fmt.Errorf("could not verify token %s: %s", token, err)
+ return
+ }
+ body, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+
+ var ti GitHubTokenUser
+ err = json.Unmarshal(body, &ti)
+ if err != nil {
+ err = fmt.Errorf("could not unmarshal token user info %q: %s", string(body), err)
+ return
+ }
+ glog.V(2).Infof("Token user info: %+v", strings.Replace(string(body), "\n", " ", -1))
+
+ err = gha.checkOrganization(token, ti.Login)
+ if err != nil {
+ err = fmt.Errorf("could not validate organization: %s", err)
+ return
+ }
+
+ return ti.Login, nil
+}
+
+func (gha *GitHubAuth) checkOrganization(token, user string) (err error) {
+ if gha.config.Organization == "" {
+ return nil
+ }
+ glog.Infof("Github API: Fetching organization membership info")
+ url := fmt.Sprintf("%s/orgs/%s/members/%s", gha.getGithubApiUri(), gha.config.Organization, user)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ err = fmt.Errorf("could not create request to get organization membership: %s", err)
+ return
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
+
+ resp, err := gha.client.Do(req)
+ if err != nil {
+ return
+ }
+
+ switch resp.StatusCode {
+ case http.StatusNoContent:
+ return nil
+ case http.StatusNotFound:
+ return fmt.Errorf("user %s is not a member of organization %s", user, gha.config.Organization)
+ case http.StatusFound:
+ return fmt.Errorf("token %s could not get membership for organization %s", token, gha.config.Organization)
+ }
+
+ return fmt.Errorf("Unknown status for membership of organization %s: %s", gha.config.Organization, resp.Status)
+}
+
+func (gha *GitHubAuth) fetchTeams(token string) ([]string, error) {
+ var allTeams GitHubTeamCollection
+
+ if gha.config.Organization == "" {
+ return nil, nil
+ }
+ glog.Infof("Github API: Fetching user teams")
+ url := fmt.Sprintf("%s/user/teams?per_page=100", gha.getGithubApiUri())
+ var err error
+
+ // Using an `i` iterator for debugging the results
+ for i := 1; url != ""; i++ {
+ var pagedTeams GitHubTeamCollection
+ resp, err := execGHExperimentalApiRequest(url, token)
+ if err != nil {
+ return nil, err
+ }
+
+ respHeaders := resp.Header
+ body, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+
+ err = json.Unmarshal(body, &pagedTeams)
+ if err != nil {
+ err = fmt.Errorf("Error parsing the JSON response while fetching teams: %s", err)
+ return nil, err
+ }
+
+ allTeams = append(allTeams, pagedTeams...)
+
+ // Do we need to paginate?
+ if link, ok := respHeaders["Link"]; ok {
+ parsedLink, _ := parseLinkHeader(link)
+ url = parsedLink.Next
+ glog.V(2).Infof("--> Page <%d>\n", i)
+ } else {
+ url = ""
+ }
+ }
+
+ // Use map instead of slice to ensure uniqueness of results
+ organizationTeamsMap := make(map[string]bool)
+ for _, item := range allTeams {
+ if item.Organization.Login == gha.config.Organization {
+ organizationTeamsMap[item.Slug] = true
+ if item.Parent != nil {
+ organizationTeamsMap[item.Parent.Slug] = true
+ }
+ }
+ }
+
+ organizationTeams := make([]string, len(organizationTeamsMap))
+ i := 0
+ for orgTeam, _ := range organizationTeamsMap {
+ organizationTeams[i] = orgTeam
+ i++
+ }
+
+ glog.V(3).Infof("All teams for the user: %v", allTeams)
+ glog.Infof("Teams for the <%s> organization: %v", gha.config.Organization, organizationTeams)
+ return organizationTeams, err
+}
+
+func (gha *GitHubAuth) validateServerToken(user string) (*TokenDBValue, error) {
+ v, err := gha.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again")
+ }
+ return nil, err
+ }
+
+ texp := v.ValidUntil.Sub(time.Now())
+ glog.V(3).Infof("Existing GitHub auth token for <%s> expires after: <%d> sec", user, int(texp.Seconds()))
+
+ glog.V(1).Infof("Token has expired. I will revalidate the access token.")
+ glog.V(3).Infof("Old token is: %+v", v)
+ tokenUser, err := gha.validateAccessToken(v.AccessToken)
+ if err != nil {
+ glog.Warningf("Token for %q failed validation: %s", user, err)
+ return nil, fmt.Errorf("server token invalid: %s", err)
+ }
+ if tokenUser != user {
+ glog.Errorf("token for wrong user: expected %s, found %s", user, tokenUser)
+ return nil, fmt.Errorf("found token for wrong user")
+ }
+
+ // Update revalidation timestamp
+ v.ValidUntil = time.Now().Add(gha.config.RevalidateAfter)
+ glog.V(3).Infof("New token is: %+v", v)
+
+ // Update token
+ _, err = gha.db.StoreToken(user, v, false)
+ if err != nil {
+ glog.Errorf("Failed to record server token: %s", err)
+ return nil, fmt.Errorf("Unable to store renewed token expiry time: %s", err)
+ }
+ glog.V(2).Infof("Successfully revalidated token")
+
+ texp = v.ValidUntil.Sub(time.Now())
+ glog.V(3).Infof("Re-validated GitHub auth token for %s. Next revalidation in %dsec.", user, int64(texp.Seconds()))
+ return v, nil
+}
+
+func (gha *GitHubAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ err := gha.db.ValidateToken(user, password)
+ if err == ExpiredToken {
+ _, err = gha.validateServerToken(user)
+ if err != nil {
+ return false, nil, err
+ }
+ } else if err != nil {
+ return false, nil, err
+ }
+
+ v, err := gha.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again")
+ }
+ return false, nil, err
+ }
+
+ return true, v.Labels, nil
+}
+
+func (gha *GitHubAuth) Stop() {
+ gha.db.Close()
+ glog.Info("Token DB closed")
+}
+
+func (gha *GitHubAuth) Name() string {
+ return "GitHub"
+}
diff --git a/auth_server/authn/gitlab_auth.go b/auth_server/authn/gitlab_auth.go
new file mode 100644
index 00000000..d6668f27
--- /dev/null
+++ b/auth_server/authn/gitlab_auth.go
@@ -0,0 +1,373 @@
+/*
+ Copyright 2016 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type GitlabTeamCollection []GitlabTeam
+
+type GitlabTeam struct {
+ Id int64 `json:"id"`
+ Url string `json:"url,omitempty"`
+ Name string `json:"name,omitempty"`
+ Slug string `json:"slug,omitempty"`
+ Organization *GitlabOrganization `json:"organization"`
+ Parent *ParentGitlabTeam `json:"parent,omitempty"`
+}
+
+type GitlabOrganization struct {
+ Login string `json:"login"`
+ Id int64 `json:"id,omitempty"`
+}
+
+type ParentGitlabTeam struct {
+ Id int64 `json:"id"`
+ Name string `json:"name,omitempty"`
+ Slug string `json:"slug,omitempty"`
+}
+
+type GitlabAuthConfig struct {
+ Organization string `yaml:"organization,omitempty"`
+ ClientId string `yaml:"client_id,omitempty"`
+ ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ LevelTokenDB *LevelDBStoreConfig `yaml:"level_token_db,omitempty"`
+ GCSTokenDB *GCSStoreConfig `yaml:"gcs_token_db,omitempty"`
+ RedisTokenDB *RedisStoreConfig `yaml:"redis_token_db,omitempty"`
+ HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
+ RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
+ GitlabWebUri string `yaml:"gitlab_web_uri,omitempty"`
+ GitlabApiUri string `yaml:"gitlab_api_uri,omitempty"`
+ RegistryUrl string `yaml:"registry_url,omitempty"`
+ GrantType string `yaml:"grant_type,omitempty"`
+ RedirectUri string `yaml:"redirect_uri,omitempty"`
+}
+
+type CodeToGitlabTokenResponse struct {
+ AccessToken string `json:"access_token,omitempty"`
+ TokenType string `json:"token_type,omitempty"`
+ ExpiresIn int64 `json:"expires_in,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty"`
+ CreatedAt int64 `json:"created_at,omitempty"`
+
+ // Returned in case of error.
+ Error string `json:"error,omitempty"`
+ ErrorDescription string `json:"error_description,omitempty"`
+}
+
+type GitlabAuthRequest struct {
+ Action string `json:"action,omitempty"`
+ Code string `json:"code,omitempty"`
+ Token string `json:"token,omitempty"`
+}
+
+type GitlabTokenUser struct {
+ Login string `json:"username,omitempty"`
+ Email string `json:"email,omitempty"`
+}
+
+type GitlabAuth struct {
+ config *GitlabAuthConfig
+ db TokenDB
+ client *http.Client
+ tmpl *template.Template
+ tmplResult *template.Template
+}
+
+func NewGitlabAuth(c *GitlabAuthConfig) (*GitlabAuth, error) {
+ var db TokenDB
+ var err error
+ var dbName string
+
+ switch {
+ case c.GCSTokenDB != nil:
+ db, err = NewGCSTokenDB(c.GCSTokenDB)
+ dbName = "GCS: " + c.GCSTokenDB.Bucket
+ case c.RedisTokenDB != nil:
+ db, err = NewRedisTokenDB(c.RedisTokenDB)
+ dbName = db.(*redisTokenDB).String()
+ default:
+ db, err = NewTokenDB(c.LevelTokenDB)
+ dbName = c.LevelTokenDB.Path
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ glog.Infof("GitLab auth token DB at %s", dbName)
+ gitlab_auth, _ := static.ReadFile("data/gitlab_auth.tmpl")
+ gitlab_auth_result, _ := static.ReadFile("data/gitlab_auth_result.tmpl")
+ return &GitlabAuth{
+ config: c,
+ db: db,
+ client: &http.Client{Timeout: c.HTTPTimeout},
+ tmpl: template.Must(template.New("gitlab_auth").Parse(string(gitlab_auth))),
+ tmplResult: template.Must(template.New("gitlab_auth_result").Parse(string(gitlab_auth_result))),
+ }, nil
+}
+
+func (glab *GitlabAuth) doGitlabAuthPage(rw http.ResponseWriter, req *http.Request) {
+ if err := glab.tmpl.Execute(rw, struct {
+ ClientId, GitlabWebUri, Organization, RedirectUri string
+ }{
+ ClientId: glab.config.ClientId,
+ GitlabWebUri: glab.getGitlabWebUri(),
+ Organization: glab.config.Organization,
+ RedirectUri: glab.config.RedirectUri}); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+func (glab *GitlabAuth) doGitlabAuthResultPage(rw http.ResponseWriter, username string, password string) {
+ if err := glab.tmplResult.Execute(rw, struct {
+ Organization, Username, Password, RegistryUrl string
+ }{Organization: glab.config.Organization,
+ Username: username,
+ Password: password,
+ RegistryUrl: glab.config.RegistryUrl}); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+func (glab *GitlabAuth) DoGitlabAuth(rw http.ResponseWriter, req *http.Request) {
+ code := req.URL.Query().Get("code")
+
+ if code != "" {
+ glab.doGitlabAuthCreateToken(rw, code)
+ } else if req.Method == "GET" {
+ glab.doGitlabAuthPage(rw, req)
+ return
+ }
+}
+
+func (glab *GitlabAuth) getGitlabApiUri() string {
+ if glab.config.GitlabApiUri != "" {
+ return glab.config.GitlabApiUri
+ } else {
+ return "https://gitlab.com/"
+ }
+}
+
+func (glab *GitlabAuth) getGitlabWebUri() string {
+ if glab.config.GitlabWebUri != "" {
+ return glab.config.GitlabWebUri
+ } else {
+ return "https://gitlab.com/api/v4"
+ }
+}
+
+func (glab *GitlabAuth) doGitlabAuthCreateToken(rw http.ResponseWriter, code string) {
+ data := url.Values{
+ "client_id": []string{glab.config.ClientId},
+ "client_secret": []string{glab.config.ClientSecret},
+ "code": []string{string(code)},
+ "grant_type": []string{glab.config.GrantType},
+ "redirect_uri": []string{glab.config.RedirectUri},
+ }
+ req, err := http.NewRequest("POST", fmt.Sprintf("%s/oauth/token", glab.getGitlabWebUri()), bytes.NewBufferString(data.Encode()))
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error creating request to GitHub auth backend: %s", err), http.StatusServiceUnavailable)
+ return
+ }
+ req.Header.Add("Accept", "application/json")
+ resp, err := glab.client.Do(req)
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error talking to GitLab auth backend: %s", err), http.StatusServiceUnavailable)
+ return
+ }
+ codeResp, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ glog.V(2).Infof("Code to token resp: %s", strings.Replace(string(codeResp), "\n", " ", -1))
+
+ var c2t CodeToTokenResponse
+ err = json.Unmarshal(codeResp, &c2t)
+ if err != nil || c2t.Error != "" || c2t.ErrorDescription != "" {
+ var et string
+ if err != nil {
+ et = err.Error()
+ } else {
+ et = fmt.Sprintf("%s: %s", c2t.Error, c2t.ErrorDescription)
+ }
+ http.Error(rw, fmt.Sprintf("Failed to get token: %s", et), http.StatusBadRequest)
+ return
+ }
+ user, err := glab.validateGitlabAccessToken(c2t.AccessToken)
+ if err != nil {
+ glog.Errorf("Newly-acquired token is invalid: %+v %s", c2t, err)
+ http.Error(rw, "Newly-acquired token is invalid", http.StatusInternalServerError)
+ return
+ }
+
+ glog.Infof("New GitLab auth token for %s", user)
+
+ v := &TokenDBValue{
+ TokenType: c2t.TokenType,
+ AccessToken: c2t.AccessToken,
+ ValidUntil: time.Now().Add(glab.config.RevalidateAfter),
+ }
+ dp, err := glab.db.StoreToken(user, v, true)
+ if err != nil {
+ glog.Errorf("Failed to record server token: %s", err)
+ http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
+ return
+ }
+ glab.doGitlabAuthResultPage(rw, user, dp)
+}
+
+func (glab *GitlabAuth) validateGitlabAccessToken(token string) (user string, err error) {
+ glog.Infof("Gitlab API: Fetching user info")
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/user", glab.getGitlabApiUri()), nil)
+
+ if err != nil {
+ err = fmt.Errorf("could not create request to get information for token %s: %s", token, err)
+ return
+ }
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+
+ resp, err := glab.client.Do(req)
+ if err != nil {
+ err = fmt.Errorf("could not verify token %s: %s", token, err)
+ return
+ }
+ body, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ var ti GitlabTokenUser
+ err = json.Unmarshal(body, &ti)
+ if err != nil {
+ err = fmt.Errorf("could not unmarshal token user info %q: %s", string(body), err)
+ return
+ }
+ glog.V(2).Infof("Token user info: %+v", strings.Replace(string(body), "\n", " ", -1))
+ return ti.Login, nil
+}
+
+func (glab *GitlabAuth) checkGitlabOrganization(token, user string) (err error) {
+ if glab.config.Organization == "" {
+ return nil
+ }
+ glog.Infof("Gitlab API: Fetching organization membership info")
+ url := fmt.Sprintf("%s/orgs/%s/members/%s", glab.getGitlabApiUri(), glab.config.Organization, user)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ err = fmt.Errorf("could not create request to get organization membership: %s", err)
+ return
+ }
+ req.Header.Add("Authorization", fmt.Sprintf("token %s", token))
+
+ resp, err := glab.client.Do(req)
+ if err != nil {
+ return
+ }
+ switch resp.StatusCode {
+ case http.StatusNoContent:
+ return nil
+ case http.StatusNotFound:
+ return fmt.Errorf("user %s is not a member of organization %s", user, glab.config.Organization)
+ case http.StatusFound:
+ return fmt.Errorf("token %s could not get membership for organization %s", token, glab.config.Organization)
+ }
+
+ return fmt.Errorf("Unknown status for membership of organization %s: %s", glab.config.Organization, resp.Status)
+}
+
+func (glab *GitlabAuth) validateGitlabServerToken(user string) (*TokenDBValue, error) {
+ v, err := glab.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again")
+ }
+ return nil, err
+ }
+
+ texp := v.ValidUntil.Sub(time.Now())
+ glog.V(3).Infof("Existing Gitlab auth token for <%s> expires after: <%d> sec", user, int(texp.Seconds()))
+
+ glog.V(1).Infof("Token has expired. I will revalidate the access token.")
+ glog.V(3).Infof("Old token is: %+v", v)
+ tokenUser, err := glab.validateGitlabAccessToken(v.AccessToken)
+ if err != nil {
+ glog.Warningf("Token for %q failed validation: %s", user, err)
+ return nil, fmt.Errorf("server token invalid: %s", err)
+ }
+ if tokenUser != user {
+ glog.Errorf("token for wrong user: expected %s, found %s", user, tokenUser)
+ return nil, fmt.Errorf("found token for wrong user")
+ }
+
+ // Update revalidation timestamp
+ v.ValidUntil = time.Now().Add(glab.config.RevalidateAfter)
+ glog.V(3).Infof("New token is: %+v", v)
+
+ // Update token
+ _, err = glab.db.StoreToken(user, v, false)
+ if err != nil {
+ glog.Errorf("Failed to record server token: %s", err)
+ return nil, fmt.Errorf("Unable to store renewed token expiry time: %s", err)
+ }
+ glog.V(2).Infof("Successfully revalidated token")
+
+ texp = v.ValidUntil.Sub(time.Now())
+ glog.V(3).Infof("Re-validated Gitlab auth token for %s. Next revalidation in %dsec.", user, int64(texp.Seconds()))
+ return v, nil
+}
+
+func (glab *GitlabAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ err := glab.db.ValidateToken(user, password)
+ if err == ExpiredToken {
+ _, err = glab.validateGitlabServerToken(user)
+ if err != nil {
+ return false, nil, err
+ }
+ } else if err != nil {
+ return false, nil, err
+ }
+
+ v, err := glab.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again")
+ }
+ return false, nil, err
+ }
+
+ return true, v.Labels, nil
+}
+
+func (glab *GitlabAuth) Stop() {
+ glab.db.Close()
+ glog.Info("Token DB closed")
+}
+
+func (glab *GitlabAuth) Name() string {
+ return "Gitlab"
+}
diff --git a/auth_server/authn/google_auth.go b/auth_server/authn/google_auth.go
index efd005ec..622a7b0a 100644
--- a/auth_server/authn/google_auth.go
+++ b/auth_server/authn/google_auth.go
@@ -21,25 +21,26 @@ import (
"errors"
"fmt"
"html/template"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"strings"
"time"
- "github.com/dchest/uniuri"
- "github.com/golang/glog"
- "github.com/syndtr/goleveldb/leveldb"
- "golang.org/x/crypto/bcrypt"
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
type GoogleAuthConfig struct {
- Domain string `yaml:"domain,omitempty"`
- ClientId string `yaml:"client_id,omitempty"`
- ClientSecret string `yaml:"client_secret,omitempty"`
- ClientSecretFile string `yaml:"client_secret_file,omitempty"`
- TokenDB string `yaml:"token_db,omitempty"`
- HTTPTimeout int `yaml:"http_timeout,omitempty"`
+ Domain string `yaml:"domain,omitempty"`
+ ClientId string `yaml:"client_id,omitempty"`
+ ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ LevelTokenDB *LevelDBStoreConfig `yaml:"level_token_db,omitempty"`
+ GCSTokenDB *GCSStoreConfig `yaml:"gcs_token_db,omitempty"`
+ RedisTokenDB *RedisStoreConfig `yaml:"redis_token_db,omitempty"`
+ HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
}
type GoogleAuthRequest struct {
@@ -120,40 +121,39 @@ type ProfileResponse struct {
// There are more fields, but we only need email.
}
-// Database-related stuff.
-const (
- tokenDBPrefix = "t:" // Keys in the database are t:email@example.com
-)
-
-// TokenDBValue is stored in the database, JSON-serialized.
-type TokenDBValue struct {
- TokenType string `json:"token_type,omitempty"` // Usually "Bearer"
- AccessToken string `json:"access_token,omitempty"`
- RefreshToken string `json:"refresh_token,omitempty"`
- ValidUntil time.Time `json:"valid_until,omitempty"`
- // DockerPassword is the temporary password we use to authenticate Docker users.
- // Gneerated at the time of token creation, stored here as a BCrypt hash.
- DockerPassword string `json:"docker_password,omitempty"`
-}
-
type GoogleAuth struct {
config *GoogleAuthConfig
- db *leveldb.DB
+ db TokenDB
client *http.Client
tmpl *template.Template
}
func NewGoogleAuth(c *GoogleAuthConfig) (*GoogleAuth, error) {
- db, err := leveldb.OpenFile(c.TokenDB, nil)
+ var db TokenDB
+ var err error
+ var dbName string
+
+ switch {
+ case c.GCSTokenDB != nil:
+ db, err = NewGCSTokenDB(c.GCSTokenDB)
+ dbName = "GCS: " + c.GCSTokenDB.Bucket
+ case c.RedisTokenDB != nil:
+ db, err = NewRedisTokenDB(c.RedisTokenDB)
+ dbName = db.(*redisTokenDB).String()
+ default:
+ db, err = NewTokenDB(c.LevelTokenDB)
+ dbName = c.LevelTokenDB.Path
+ }
if err != nil {
return nil, err
}
- glog.Infof("Google auth token DB at %s", c.TokenDB)
+ glog.Infof("Google auth token DB at %s", dbName)
+ google_auth, _ := static.ReadFile("data/google_auth.tmpl")
return &GoogleAuth{
config: c,
db: db,
- client: &http.Client{Timeout: 10 * time.Second},
- tmpl: template.Must(template.New("google_auth").Parse(string(MustAsset("data/google_auth.tmpl")))),
+ client: &http.Client{Timeout: c.HTTPTimeout},
+ tmpl: template.Must(template.New("google_auth").Parse(string(google_auth))),
}, nil
}
@@ -162,7 +162,7 @@ func (ga *GoogleAuth) DoGoogleAuth(rw http.ResponseWriter, req *http.Request) {
ga.doGoogleAuthPage(rw, req)
return
}
- gauthRequest, _ := ioutil.ReadAll(req.Body)
+ gauthRequest, _ := io.ReadAll(req.Body)
glog.V(2).Infof("gauth request: %s", string(gauthRequest))
var gar GoogleAuthRequest
err := json.Unmarshal(gauthRequest, &gar)
@@ -203,7 +203,7 @@ func (ga *GoogleAuth) doGoogleAuthCreateToken(rw http.ResponseWriter, code strin
http.Error(rw, fmt.Sprintf("Error talking to Google auth backend: %s", err), http.StatusServiceUnavailable)
return
}
- codeResp, _ := ioutil.ReadAll(resp.Body)
+ codeResp, _ := io.ReadAll(resp.Body)
resp.Body.Close()
glog.V(2).Infof("Code to token resp: %s", strings.Replace(string(codeResp), "\n", " ", -1))
@@ -240,24 +240,20 @@ func (ga *GoogleAuth) doGoogleAuthCreateToken(rw http.ResponseWriter, code strin
glog.Infof("New Google auth token for %s (exp %d)", user, c2t.ExpiresIn)
- dp := uniuri.New()
- dph, _ := bcrypt.GenerateFromPassword([]byte(dp), bcrypt.DefaultCost)
-
v := &TokenDBValue{
- TokenType: c2t.TokenType,
- AccessToken: c2t.AccessToken,
- RefreshToken: c2t.RefreshToken,
- ValidUntil: time.Now().Add(time.Duration(c2t.ExpiresIn-30) * time.Second),
- DockerPassword: string(dph),
+ TokenType: c2t.TokenType,
+ AccessToken: c2t.AccessToken,
+ RefreshToken: c2t.RefreshToken,
+ ValidUntil: time.Now().Add(time.Duration(c2t.ExpiresIn-30) * time.Second),
}
- err = ga.setServerToken(user, v)
+ dp, err := ga.db.StoreToken(user, v, true)
if err != nil {
glog.Errorf("Failed to record server token: %s", err)
http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
return
}
- fmt.Fprintf(rw, `Server logged in; now run "docker login", use %s as login and %s as password.`, user, dp)
+ fmt.Fprintf(rw, `Server logged in; now run "docker login YOUR_REGISTRY_FQDN", use %s as login and %s as password.`, user, dp)
}
func (ga *GoogleAuth) getIDTokenInfo(token string) (*GoogleTokenInfo, error) {
@@ -266,7 +262,7 @@ func (ga *GoogleAuth) getIDTokenInfo(token string) (*GoogleTokenInfo, error) {
if err != nil {
return nil, fmt.Errorf("could not verify token %s: %s", token, err)
}
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var ti GoogleTokenInfo
@@ -307,10 +303,6 @@ func (ga *GoogleAuth) checkDomain(email string) error {
return nil
}
-func getDBKey(user string) []byte {
- return []byte(fmt.Sprintf("%s%s", tokenDBPrefix, user))
-}
-
// https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
func (ga *GoogleAuth) refreshAccessToken(refreshToken string) (rtr RefreshTokenResponse, err error) {
resp, err := ga.client.PostForm(
@@ -325,7 +317,7 @@ func (ga *GoogleAuth) refreshAccessToken(refreshToken string) (rtr RefreshTokenR
err = fmt.Errorf("Error talking to Google auth backend: %s", err)
return
}
- respStr, _ := ioutil.ReadAll(resp.Body)
+ respStr, _ := io.ReadAll(resp.Body)
glog.V(2).Infof("Refresh token resp: %s", strings.Replace(string(respStr), "\n", " ", -1))
err = json.Unmarshal(respStr, &rtr)
@@ -342,7 +334,7 @@ func (ga *GoogleAuth) validateAccessToken(toktype, token string) (user string, e
if err != nil {
return
}
- respStr, _ := ioutil.ReadAll(resp.Body)
+ respStr, _ := io.ReadAll(resp.Body)
glog.V(2).Infof("Access token validation rrsponse: %s", strings.Replace(string(respStr), "\n", " ", -1))
var pr ProfileResponse
err = json.Unmarshal(respStr, &pr)
@@ -356,26 +348,8 @@ func (ga *GoogleAuth) validateAccessToken(toktype, token string) (user string, e
return pr.Email, nil
}
-func (ga *GoogleAuth) getDBValue(user string) (*TokenDBValue, error) {
- valueStr, err := ga.db.Get(getDBKey(user), nil)
- switch {
- case err == leveldb.ErrNotFound:
- return nil, nil
- case err != nil:
- glog.Errorf("error accessing token db: %s", err)
- return nil, fmt.Errorf("error accessing token db: %s", err)
- }
- var dbv TokenDBValue
- err = json.Unmarshal(valueStr, &dbv)
- if err != nil {
- glog.Errorf("bad DB value for %q (%q): %s", user, string(valueStr), err)
- return nil, fmt.Errorf("bad DB value", err)
- }
- return &dbv, nil
-}
-
func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
- v, err := ga.getDBValue(user)
+ v, err := ga.db.GetValue(user)
if err != nil || v == nil {
if err == nil {
err = errors.New("no db value, please sign out and sign in again.")
@@ -392,7 +366,7 @@ func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
v.AccessToken = rtr.AccessToken
v.ValidUntil = time.Now().Add(time.Duration(rtr.ExpiresIn-30) * time.Second)
glog.Infof("Refreshed auth token for %s (exp %d)", user, rtr.ExpiresIn)
- err = ga.setServerToken(user, v)
+ _, err = ga.db.StoreToken(user, v, false)
if err != nil {
glog.Errorf("Failed to record refreshed token: %s", err)
return nil, fmt.Errorf("failed to record refreshed token: %s", err)
@@ -412,26 +386,6 @@ func (ga *GoogleAuth) validateServerToken(user string) (*TokenDBValue, error) {
return v, nil
}
-func (ga *GoogleAuth) setServerToken(user string, v *TokenDBValue) error {
- data, err := json.Marshal(v)
- if err != nil {
- return err
- }
- err = ga.db.Put(getDBKey(user), data, nil)
- if err != nil {
- glog.Errorf("failed to set token data for %s: %s", user, err)
- }
- glog.V(2).Infof("Server tokens for %s: %s", user, string(data))
- return err
-}
-
-func (ga *GoogleAuth) deleteServerToken(user string) {
- glog.V(1).Infof("deleting token for %s", user)
- if err := ga.db.Delete(getDBKey(user), nil); err != nil {
- glog.Errorf("failed to delete %s: %s", user, err)
- }
-}
-
func (ga *GoogleAuth) doGoogleAuthCheck(rw http.ResponseWriter, token string) {
// First, authenticate web user.
ti, err := ga.getIDTokenInfo(token)
@@ -457,28 +411,24 @@ func (ga *GoogleAuth) doGoogleAuthSignOut(rw http.ResponseWriter, token string)
http.Error(rw, fmt.Sprintf("Could not verify user token: %s", err), http.StatusBadRequest)
return
}
- ga.deleteServerToken(ti.Email)
+ err = ga.db.DeleteToken(ti.Email)
+ if err != nil {
+ glog.Error(err)
+ }
fmt.Fprint(rw, "signed out")
}
-func (ga *GoogleAuth) Authenticate(user string, password PasswordString) (bool, error) {
- dbv, err := ga.getDBValue(user)
- if err != nil {
- return false, err
- }
- if dbv == nil {
- return false, NoMatch
- }
- if time.Now().After(dbv.ValidUntil) {
- dbv, err = ga.validateServerToken(user)
+func (ga *GoogleAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ err := ga.db.ValidateToken(user, password)
+ if err == ExpiredToken {
+ _, err = ga.validateServerToken(user)
if err != nil {
- return false, err
+ return false, nil, err
}
+ } else if err != nil {
+ return false, nil, err
}
- if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
- return false, nil
- }
- return true, nil
+ return true, nil, nil
}
func (ga *GoogleAuth) Stop() {
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
old mode 100755
new mode 100644
index ad24c6af..cc837cd9
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -17,25 +17,35 @@
package authn
import (
- "bytes"
"crypto/tls"
+ "crypto/x509"
"fmt"
"io/ioutil"
"strings"
+ "github.com/cesanta/glog"
"github.com/go-ldap/ldap"
- "github.com/golang/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
+type LabelMap struct {
+ Attribute string `yaml:"attribute,omitempty"`
+ ParseCN bool `yaml:"parse_cn,omitempty"`
+ LowerCase bool `yaml:"lower_case",omitempty"`
+}
+
type LDAPAuthConfig struct {
- Addr string `yaml:"addr,omitempty"`
- StartTLS bool `yaml:"tls,omitempty"`
- Base string `yaml:"base,omitempty"`
- Filter string `yaml:"filter,omitempty"`
- BindDN string `yaml:"bind_dn,omitempty"`
- BindPasswordFile string `yaml:"bind_password_file,omitempty"`
- GroupBaseDN string `yaml:"group_base_dn,omitempty"`
- GroupFilter string `yaml:"group_filter,omitempty"`
+ Addr string `yaml:"addr,omitempty"`
+ TLS string `yaml:"tls,omitempty"`
+ InsecureTLSSkipVerify bool `yaml:"insecure_tls_skip_verify,omitempty"`
+ CACertificate string `yaml:"ca_certificate,omitempty"`
+ Base string `yaml:"base,omitempty"`
+ Filter string `yaml:"filter,omitempty"`
+ BindDN string `yaml:"bind_dn,omitempty"`
+ BindPasswordFile string `yaml:"bind_password_file,omitempty"`
+ LabelMaps map[string]LabelMap `yaml:"labels,omitempty"`
+ InitialBindAsUser bool `yaml:"initial_bind_as_user,omitempty"`
}
type LDAPAuth struct {
@@ -43,47 +53,79 @@ type LDAPAuth struct {
}
func NewLDAPAuth(c *LDAPAuthConfig) (*LDAPAuth, error) {
+ if c.TLS == "" && strings.HasSuffix(c.Addr, ":636") {
+ c.TLS = "always"
+ }
return &LDAPAuth{
config: c,
}, nil
}
//How to authenticate user, please refer to https://github.com/go-ldap/ldap/blob/master/example_test.go#L166
-func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool, error) {
- if account == "" {
- return false, NoMatch
+func (la *LDAPAuth) Authenticate(account string, password api.PasswordString) (bool, api.Labels, error) {
+ if account == "" || password == "" {
+ return false, nil, api.NoMatch
}
l, err := la.ldapConnection()
if err != nil {
- return false, err
+ return false, nil, err
}
defer l.Close()
- // First bind with a read only user, to prevent the following search won't perform any write action
- if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
- return false, bindErr
- }
-
account = la.escapeAccountInput(account)
+ if la.config.InitialBindAsUser {
+ if bindErr := la.bindInitialAsUser(l, account, password); bindErr != nil {
+ if ldap.IsErrorWithCode(bindErr, ldap.LDAPResultInvalidCredentials) {
+ return false, nil, api.WrongPass
+ }
+ return false, nil, bindErr
+ }
+ } else {
+ // First bind with a read only user, to prevent the following search won't perform any write action
+ if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
+ return false, nil, bindErr
+ }
+ }
filter := la.getFilter(account)
- accountEntryDN, uSearchErr := la.ldapSearch(l, &la.config.Base, &filter, &[]string{})
+
+ labelAttributes, labelsConfigErr := la.getLabelAttributes()
+ if labelsConfigErr != nil {
+ return false, nil, labelsConfigErr
+ }
+
+ accountEntryDN, entryAttrMap, uSearchErr := la.ldapSearch(l, &la.config.Base, &filter, &labelAttributes)
if uSearchErr != nil {
- return false, uSearchErr
+ return false, nil, uSearchErr
+ }
+ if accountEntryDN == "" {
+ return false, nil, api.NoMatch // User does not exist
}
+
// Bind as the user to verify their password
if len(accountEntryDN) > 0 {
err := l.Bind(accountEntryDN, string(password))
if err != nil {
- return false, err
+ if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
+ return false, nil, nil
+ }
+ return false, nil, err
}
}
// Rebind as the read only user for any futher queries
- if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
- return false, bindErr
+ if !la.config.InitialBindAsUser {
+ if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
+ return false, nil, bindErr
+ }
+ }
+
+ // Extract labels from the attribute values
+ labels, labelsExtractErr := la.getLabelsFromMap(entryAttrMap)
+ if labelsExtractErr != nil {
+ return false, nil, labelsExtractErr
}
- return true, nil
+ return true, labels, nil
}
func (la *LDAPAuth) bindReadOnlyUser(l *ldap.Conn) error {
@@ -102,6 +144,22 @@ func (la *LDAPAuth) bindReadOnlyUser(l *ldap.Conn) error {
return nil
}
+func (la *LDAPAuth) getInitialBindDN(account string) string {
+ initialBindDN := strings.NewReplacer("${account}", account).Replace(la.config.BindDN)
+ glog.V(2).Infof("Initial BindDN is %s", initialBindDN)
+ return initialBindDN
+}
+
+func (la *LDAPAuth) bindInitialAsUser(l *ldap.Conn, account string, password api.PasswordString) error {
+ accountEntryDN := la.getInitialBindDN(account)
+ glog.V(2).Infof("Bind as initial user (DN = %s)", accountEntryDN)
+ err := l.Bind(accountEntryDN, string(password))
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
//To prevent LDAP injection, some characters must be escaped for searching
//e.g. char '\' will be replaced by hex '\5c'
//Filter meta chars are choosen based on filter complier code
@@ -124,17 +182,43 @@ func (la *LDAPAuth) escapeAccountInput(account string) string {
}
func (la *LDAPAuth) ldapConnection() (*ldap.Conn, error) {
- glog.V(2).Infof("Dial: starting...%s", la.config.Addr)
- l, err := ldap.Dial("tcp", fmt.Sprintf("%s", la.config.Addr))
- if err != nil {
- return nil, err
+ var l *ldap.Conn
+ var err error
+
+ tlsConfig := &tls.Config{InsecureSkipVerify: true}
+ if !la.config.InsecureTLSSkipVerify {
+ addr := strings.Split(la.config.Addr, ":")
+ if la.config.CACertificate != "" {
+ pool := x509.NewCertPool()
+ pem, err := ioutil.ReadFile(la.config.CACertificate)
+ if err != nil {
+ return nil, fmt.Errorf("Error loading CA File: %s", err)
+ }
+ ok := pool.AppendCertsFromPEM(pem)
+ if !ok {
+ return nil, fmt.Errorf("Error loading CA File: Couldn't parse PEM in: %s", la.config.CACertificate)
+ }
+ tlsConfig = &tls.Config{InsecureSkipVerify: false, ServerName: addr[0], RootCAs: pool}
+ } else {
+ tlsConfig = &tls.Config{InsecureSkipVerify: false, ServerName: addr[0]}
+ }
}
- if la.config.StartTLS {
- glog.V(2).Infof("StartTLS...")
- err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
- if err != nil {
- return nil, err
+
+ if la.config.TLS == "" || la.config.TLS == "none" || la.config.TLS == "starttls" {
+ glog.V(2).Infof("Dial: starting...%s", la.config.Addr)
+ l, err = ldap.Dial("tcp", fmt.Sprintf("%s", la.config.Addr))
+ if err == nil && la.config.TLS == "starttls" {
+ glog.V(2).Infof("StartTLS...")
+ if tlserr := l.StartTLS(tlsConfig); tlserr != nil {
+ return nil, tlserr
+ }
}
+ } else if la.config.TLS == "always" {
+ glog.V(2).Infof("DialTLS: starting...%s", la.config.Addr)
+ l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s", la.config.Addr), tlsConfig)
+ }
+ if err != nil {
+ return nil, err
}
return l, nil
}
@@ -147,9 +231,9 @@ func (la *LDAPAuth) getFilter(account string) string {
//ldap search and return required attributes' value from searched entries
//default return entry's DN value if you leave attrs array empty
-func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, attrs *[]string) (string, error) {
+func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, attrs *[]string) (string, map[string][]string, error) {
if l == nil {
- return "", fmt.Errorf("No ldap connection!")
+ return "", nil, fmt.Errorf("No ldap connection!")
}
glog.V(2).Infof("Searching...basedDN:%s, filter:%s", *baseDN, *filter)
searchRequest := ldap.NewSearchRequest(
@@ -160,28 +244,87 @@ func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, att
nil)
sr, err := l.Search(searchRequest)
if err != nil {
- return "", err
+ return "", nil, err
}
- if len(sr.Entries) != 1 {
- return "", fmt.Errorf("User does not exist or too many entries returned.")
+ if len(sr.Entries) == 0 {
+ return "", nil, nil // User does not exist
+ } else if len(sr.Entries) > 1 {
+ return "", nil, fmt.Errorf("Too many entries returned.")
}
- var buffer bytes.Buffer
+ attributes := make(map[string][]string)
+ var entryDn string
for _, entry := range sr.Entries {
+ entryDn = entry.DN
if len(*attrs) == 0 {
- glog.V(2).Infof("Entry DN = %s", entry.DN)
- buffer.WriteString(entry.DN)
+ glog.V(2).Infof("Entry DN = %s", entryDn)
} else {
for _, attr := range *attrs {
- values := strings.Join(entry.GetAttributeValues(attr), " ")
- glog.V(2).Infof("Entry %s = %s", attr, values)
- buffer.WriteString(values)
+ values := entry.GetAttributeValues(attr)
+ glog.V(2).Infof("Entry %s = %s", attr, strings.Join(values, "\n"))
+ attributes[attr] = values
+ }
+ }
+ }
+
+ return entryDn, attributes, nil
+}
+
+func (la *LDAPAuth) getLabelAttributes() ([]string, error) {
+ labelAttributes := make([]string, len(la.config.LabelMaps))
+ i := 0
+ for key, mapping := range la.config.LabelMaps {
+ if mapping.Attribute == "" {
+ return nil, fmt.Errorf("Label %s is missing 'attribute' to map from", key)
+ }
+ labelAttributes[i] = mapping.Attribute
+ i++
+ }
+ return labelAttributes, nil
+}
+
+func (la *LDAPAuth) getLabelsFromMap(attrMap map[string][]string) (map[string][]string, error) {
+ labels := make(map[string][]string)
+ for key, mapping := range la.config.LabelMaps {
+ if mapping.Attribute == "" {
+ return nil, fmt.Errorf("Label %s is missing 'attribute' to map from", key)
+ }
+
+ mappingValues := attrMap[mapping.Attribute]
+ if mappingValues != nil {
+ if mapping.ParseCN {
+ // shorten attribute to its common name
+ for i, value := range mappingValues {
+ cn := la.getCNFromDN(value)
+ mappingValues[i] = cn
+ }
+ }
+ if mapping.LowerCase {
+ for i, value := range mappingValues {
+ mappingValues[i] = strings.ToLower(value)
+ }
+ }
+ labels[key] = mappingValues
+ }
+ }
+ return labels, nil
+}
+
+func (la *LDAPAuth) getCNFromDN(dn string) string {
+ parsedDN, err := ldap.ParseDN(dn)
+ if err != nil || len(parsedDN.RDNs) > 0 {
+ for _, rdn := range parsedDN.RDNs {
+ for _, rdnAttr := range rdn.Attributes {
+ if strings.ToUpper(rdnAttr.Type) == "CN" {
+ return rdnAttr.Value
+ }
}
}
}
- return buffer.String(), nil
+ // else try using raw DN
+ return dn
}
func (la *LDAPAuth) Stop() {
diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go
new file mode 100644
index 00000000..db546be4
--- /dev/null
+++ b/auth_server/authn/mongo_auth.go
@@ -0,0 +1,149 @@
+/*
+ Copyright 2015 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/cesanta/glog"
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+ "github.com/cesanta/docker_auth/auth_server/mgo_session"
+)
+
+type MongoAuthConfig struct {
+ MongoConfig *mgo_session.Config `yaml:"dial_info,omitempty"`
+ Collection string `yaml:"collection,omitempty"`
+}
+
+type MongoAuth struct {
+ config *MongoAuthConfig
+ session *mongo.Client
+ Collection string `yaml:"collection,omitempty"`
+}
+
+type authUserEntry struct {
+ Username *string `yaml:"username,omitempty" json:"username,omitempty"`
+ Password *string `yaml:"password,omitempty" json:"password,omitempty"`
+ Labels api.Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
+}
+
+func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
+ // Attempt to create new mongo session.
+ session, err := mgo_session.New(c.MongoConfig)
+ if err != nil {
+ return nil, err
+ }
+ // determine collection
+ collection := session.Database(c.MongoConfig.DialInfo.Database).Collection(c.Collection)
+
+ // Create username index obj
+ index := mongo.IndexModel{
+ Keys: bson.M{"username": 1},
+ Options: options.Index().SetUnique(true),
+ }
+
+ // Enforce a username index.
+ // mongodb will do no operation if index still exists.
+ // see: https://pkg.go.dev/go.mongodb.org/mongo-driver/mongo#Collection.Indexes
+ _, erri := collection.Indexes().CreateOne(context.TODO(), index)
+ if erri != nil {
+ fmt.Println(erri.Error())
+ return nil, erri
+ }
+
+ return &MongoAuth{
+ config: c,
+ session: session,
+ }, nil
+}
+
+func (mauth *MongoAuth) Authenticate(account string, password api.PasswordString) (bool, api.Labels, error) {
+ for true {
+ result, labels, err := mauth.authenticate(account, password)
+ if err == io.EOF {
+ glog.Warningf("EOF error received from Mongo. Retrying connection")
+ time.Sleep(time.Second)
+ continue
+ }
+ return result, labels, err
+ }
+
+ return false, nil, errors.New("Unable to communicate with Mongo.")
+}
+
+func (mauth *MongoAuth) authenticate(account string, password api.PasswordString) (bool, api.Labels, error) {
+
+ // Get Users from MongoDB
+ glog.V(2).Infof("Checking user %s against Mongo Users. DB: %s, collection:%s",
+ account, mauth.config.MongoConfig.DialInfo.Database, mauth.config.Collection)
+ var dbUserRecord authUserEntry
+ collection := mauth.session.Database(mauth.config.MongoConfig.DialInfo.Database).Collection(mauth.config.Collection)
+
+
+ filter := bson.D{{"username", account}}
+ err := collection.FindOne(context.TODO(), filter).Decode(&dbUserRecord)
+
+ // If we connect and get no results we return a NoMatch so auth can fall-through
+ if err == mongo.ErrNoDocuments {
+ return false, nil, api.NoMatch
+ } else if err != nil {
+ return false, nil, err
+ }
+
+ // Validate db password against passed password
+ if dbUserRecord.Password != nil {
+ if bcrypt.CompareHashAndPassword([]byte(*dbUserRecord.Password), []byte(password)) != nil {
+ return false, nil, nil
+ }
+ }
+
+ // Auth success
+ return true, dbUserRecord.Labels, nil
+}
+
+// Validate ensures that any custom config options
+// in a Config are set correctly.
+func (c *MongoAuthConfig) Validate(configKey string) error {
+ //First validate the mongo config.
+ if err := c.MongoConfig.Validate(configKey); err != nil {
+ return err
+ }
+
+ // Now check additional config fields.
+ if c.Collection == "" {
+ return fmt.Errorf("%s.collection is required", configKey)
+ }
+
+ return nil
+}
+
+func (ma *MongoAuth) Stop() {
+
+}
+
+func (ga *MongoAuth) Name() string {
+ return "MongoDB"
+}
diff --git a/auth_server/authn/oidc_auth.go b/auth_server/authn/oidc_auth.go
new file mode 100644
index 00000000..743c168a
--- /dev/null
+++ b/auth_server/authn/oidc_auth.go
@@ -0,0 +1,401 @@
+/*
+ Copyright 2015 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "golang.org/x/oauth2"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+// All configuration options
+type OIDCAuthConfig struct {
+ // --- necessary ---
+ // URL of the authentication provider. Must be able to serve the /.well-known/openid-configuration
+ Issuer string `yaml:"issuer,omitempty"`
+ // URL of the auth server. Has to end with /oidc_auth
+ RedirectURL string `yaml:"redirect_url,omitempty"`
+ // ID and secret, priovided by the OIDC provider after registration of the auth server
+ ClientId string `yaml:"client_id,omitempty"`
+ ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ // path where the tokendb should be stored within the container
+ LevelTokenDB *LevelDBStoreConfig `yaml:"level_token_db,omitempty"`
+ GCSTokenDB *GCSStoreConfig `yaml:"gcs_token_db,omitempty"`
+ RedisTokenDB *RedisStoreConfig `yaml:"redis_token_db,omitempty"`
+ // --- optional ---
+ HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
+ // the URL of the docker registry. Used to generate a full docker login command after authentication
+ RegistryURL string `yaml:"registry_url,omitempty"`
+ // --- optional ---
+ // String claim to use for the username
+ UserClaim string `yaml:"user_claim,omitempty"`
+ // --- optional ---
+ // []string to add as labels.
+ LabelsClaims []string `yaml:"labels_claims,omitempty"`
+ // --- optional ---
+ Scopes []string `yaml:"scopes,omitempty"`
+}
+
+// OIDCRefreshTokenResponse is sent by OIDC provider in response to the grant_type=refresh_token request.
+type OIDCRefreshTokenResponse struct {
+ AccessToken string `json:"access_token,omitempty"`
+ ExpiresIn int64 `json:"expires_in,omitempty"`
+ TokenType string `json:"token_type,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty"`
+
+ // Returned in case of error.
+ Error string `json:"error,omitempty"`
+ ErrorDescription string `json:"error_description,omitempty"`
+}
+
+// The specific OIDC authenticator
+type OIDCAuth struct {
+ config *OIDCAuthConfig
+ db TokenDB
+ client *http.Client
+ tmpl *template.Template
+ tmplResult *template.Template
+ ctx context.Context
+ provider *oidc.Provider
+ verifier *oidc.IDTokenVerifier
+ oauth oauth2.Config
+}
+
+/*
+Creates everything necessary for OIDC auth.
+*/
+func NewOIDCAuth(c *OIDCAuthConfig) (*OIDCAuth, error) {
+ var db TokenDB
+ var err error
+ var dbName string
+
+ switch {
+ case c.GCSTokenDB != nil:
+ db, err = NewGCSTokenDB(c.GCSTokenDB)
+ dbName = "GCS: " + c.GCSTokenDB.Bucket
+ case c.RedisTokenDB != nil:
+ db, err = NewRedisTokenDB(c.RedisTokenDB)
+ dbName = db.(*redisTokenDB).String()
+ default:
+ db, err = NewTokenDB(c.LevelTokenDB)
+ dbName = c.LevelTokenDB.Path
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ glog.Infof("OIDC auth token DB at %s", dbName)
+ ctx := context.Background()
+ oidcAuth, _ := static.ReadFile("data/oidc_auth.tmpl")
+ oidcAuthResult, _ := static.ReadFile("data/oidc_auth_result.tmpl")
+
+ prov, err := oidc.NewProvider(ctx, c.Issuer)
+ if err != nil {
+ return nil, err
+ }
+ conf := oauth2.Config{
+ ClientID: c.ClientId,
+ ClientSecret: c.ClientSecret,
+ Endpoint: prov.Endpoint(),
+ RedirectURL: c.RedirectURL,
+ Scopes: c.Scopes,
+ }
+ return &OIDCAuth{
+ config: c,
+ db: db,
+ client: &http.Client{Timeout: c.HTTPTimeout},
+ tmpl: template.Must(template.New("oidc_auth").Parse(string(oidcAuth))),
+ tmplResult: template.Must(template.New("oidc_auth_result").Parse(string(oidcAuthResult))),
+ ctx: ctx,
+ provider: prov,
+ verifier: prov.Verifier(&oidc.Config{ClientID: conf.ClientID}),
+ oauth: conf,
+ }, nil
+}
+
+/*
+This function will be used by the server if the OIDC auth method is selected. It starts the page for OIDC login or
+requests an access token by using the code given by the OIDC provider.
+*/
+func (ga *OIDCAuth) DoOIDCAuth(rw http.ResponseWriter, req *http.Request) {
+ code := req.URL.Query().Get("code")
+ if code != "" {
+ ga.doOIDCAuthCreateToken(rw, code)
+ } else if req.Method == "GET" {
+ ga.doOIDCAuthPage(rw)
+ } else {
+ http.Error(rw, "Invalid auth request", http.StatusBadRequest)
+ }
+}
+
+/*
+Executes tmpl for the OIDC login page.
+*/
+func (ga *OIDCAuth) doOIDCAuthPage(rw http.ResponseWriter) {
+ if err := ga.tmpl.Execute(rw, struct {
+ AuthEndpoint, RedirectURI, ClientId, Scope string
+ }{
+ AuthEndpoint: ga.provider.Endpoint().AuthURL,
+ RedirectURI: ga.oauth.RedirectURL,
+ ClientId: ga.oauth.ClientID,
+ Scope: strings.Join(ga.config.Scopes, " "),
+ }); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+/*
+Executes tmplResult for the result of the login process.
+*/
+func (ga *OIDCAuth) doOIDCAuthResultPage(rw http.ResponseWriter, un string, pw string) {
+ if err := ga.tmplResult.Execute(rw, struct {
+ Username, Password, RegistryUrl string
+ }{
+ Username: un,
+ Password: pw,
+ RegistryUrl: ga.config.RegistryURL,
+ }); err != nil {
+ http.Error(rw, fmt.Sprintf("Template error: %s", err), http.StatusInternalServerError)
+ }
+}
+
+/*
+Requests an OIDC token by using the code that was provided by the OIDC provider. If it was successfull,
+the access token and refresh token is used to create a new token for the users mail address, which is taken from the ID
+token.
+*/
+func (ga *OIDCAuth) doOIDCAuthCreateToken(rw http.ResponseWriter, code string) {
+
+ tok, err := ga.oauth.Exchange(ga.ctx, code)
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Error talking to OIDC auth backend: %s", err), http.StatusInternalServerError)
+ return
+ }
+ rawIdTok, ok := tok.Extra("id_token").(string)
+ if !ok {
+ http.Error(rw, "No id_token field in oauth2 token.", http.StatusInternalServerError)
+ return
+ }
+ idTok, err := ga.verifier.Verify(ga.ctx, rawIdTok)
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("Failed to verify ID token: %s", err), http.StatusInternalServerError)
+ return
+ }
+ var claims map[string]interface{}
+ if err := idTok.Claims(&claims); err != nil {
+ http.Error(rw, fmt.Sprintf("Failed to get claims from ID token: %s", err), http.StatusInternalServerError)
+ return
+ }
+ username, _ := claims[ga.config.UserClaim].(string)
+ if username == "" {
+ http.Error(rw, fmt.Sprintf("No %q claim in ID token", ga.config.UserClaim), http.StatusInternalServerError)
+ return
+ }
+
+ glog.V(2).Infof("New OIDC auth token for %s (Current time: %s, expiration time: %s)", username, time.Now().String(), tok.Expiry.String())
+
+ dbVal := &TokenDBValue{
+ TokenType: tok.TokenType,
+ AccessToken: tok.AccessToken,
+ RefreshToken: tok.RefreshToken,
+ ValidUntil: tok.Expiry.Add(time.Duration(-30) * time.Second),
+ Labels: ga.getLabels(claims),
+ }
+ dp, err := ga.db.StoreToken(username, dbVal, true)
+ if err != nil {
+ glog.Errorf("Failed to record server token: %s", err)
+ http.Error(rw, "Failed to record server token: %s", http.StatusInternalServerError)
+ return
+ }
+
+ ga.doOIDCAuthResultPage(rw, username, dp)
+}
+
+func (ga *OIDCAuth) getLabels(claims map[string]interface{}) api.Labels {
+ labels := make(api.Labels, len(ga.config.LabelsClaims))
+ for _, claim := range ga.config.LabelsClaims {
+ values, _ := claims[claim].([]interface{})
+ for _, v := range values {
+ if str, _ := v.(string); str != "" {
+ labels[claim] = append(labels[claim], str)
+ }
+ }
+ }
+ return labels
+}
+
+/*
+Refreshes the access token of the user. Not usable with all OIDC provider, since not all provide refresh tokens.
+*/
+func (ga *OIDCAuth) refreshAccessToken(refreshToken string) (rtr OIDCRefreshTokenResponse, err error) {
+
+ url := ga.provider.Endpoint().TokenURL
+ pl := strings.NewReader(fmt.Sprintf(
+ "grant_type=refresh_token&client_id=%s&client_secret=%s&refresh_token=%s",
+ ga.oauth.ClientID, ga.oauth.ClientSecret, refreshToken))
+ req, err := http.NewRequest("POST", url, pl)
+ if err != nil {
+ err = fmt.Errorf("could not create refresh request: %s", err)
+ return
+ }
+ req.Header.Add("content-type", "application/x-www-form-urlencoded")
+
+ resp, err := ga.client.Do(req)
+ if err != nil {
+ err = fmt.Errorf("error talking to OIDC auth backend: %s", err)
+ return
+ }
+ respStr, _ := io.ReadAll(resp.Body)
+ glog.V(2).Infof("Refresh token resp: %s", strings.Replace(string(respStr), "\n", " ", -1))
+
+ err = json.Unmarshal(respStr, &rtr)
+ if err != nil {
+ err = fmt.Errorf("error in reading response of refresh request: %s", err)
+ return
+ }
+ if rtr.Error != "" || rtr.ErrorDescription != "" {
+ err = fmt.Errorf("%s: %s", rtr.Error, rtr.ErrorDescription)
+ return
+ }
+ return rtr, err
+}
+
+/*
+In case the DB token is expired, this function uses the refresh token and tries to refresh the access token stored in the
+DB. Afterwards, checks if the access token really authenticates the user trying to log in.
+*/
+func (ga *OIDCAuth) validateServerToken(user string) (*TokenDBValue, error) {
+ v, err := ga.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again")
+ }
+ return nil, err
+ }
+ if v.RefreshToken == "" {
+ return nil, errors.New("refresh of your session is not possible. Please sign out and sign in again")
+ }
+
+ glog.V(2).Infof("Refreshing token for %s", user)
+ rtr, err := ga.refreshAccessToken(v.RefreshToken)
+ if err != nil {
+ glog.Warningf("Failed to refresh token for %q: %s", user, err)
+ return nil, fmt.Errorf("failed to refresh token: %s", err)
+ }
+ v.AccessToken = rtr.AccessToken
+ v.ValidUntil = time.Now().Add(time.Duration(rtr.ExpiresIn-30) * time.Second)
+ glog.Infof("Refreshed auth token for %s (exp %d)", user, rtr.ExpiresIn)
+ _, err = ga.db.StoreToken(user, v, false)
+ if err != nil {
+ glog.Errorf("Failed to record refreshed token: %s", err)
+ return nil, fmt.Errorf("failed to record refreshed token: %s", err)
+ }
+ tokUser, err := ga.provider.UserInfo(ga.ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: v.AccessToken,
+ TokenType: v.TokenType,
+ RefreshToken: v.RefreshToken,
+ Expiry: v.ValidUntil,
+ }))
+ if err != nil {
+ glog.Warningf("Token for %q failed validation: %s", user, err)
+ return nil, fmt.Errorf("server token invalid: %s", err)
+ }
+
+ var claims map[string]interface{}
+ if err := tokUser.Claims(&claims); err != nil {
+ glog.Errorf("error retrieving claims: %v", err)
+ return nil, fmt.Errorf("error retrieving claims: %w", err)
+ }
+ claimUsername, _ := claims[ga.config.UserClaim].(string)
+ if claimUsername != user {
+ glog.Errorf("token for wrong user: expected %s, found %s", user, claimUsername)
+ return nil, fmt.Errorf("found token for wrong user")
+ }
+ texp := v.ValidUntil.Sub(time.Now())
+ glog.V(1).Infof("Validated OIDC auth token for %s (exp %d)", user, int(texp.Seconds()))
+ return v, nil
+}
+
+/*
+First checks if OIDC token is valid. Then delete the corresponding DB token from the database. The user is now signed out
+Not deleted because maybe it will be implemented in the future.
+*/
+//func (ga *OIDCAuth) doOIDCAuthSignOut(rw http.ResponseWriter, token string) {
+// // Authenticate web user.
+// ui, err := ga.validateIDToken(token)
+// if err != nil || ui == ""{
+// http.Error(rw, fmt.Sprintf("Could not verify user token: %s", err), http.StatusBadRequest)
+// return
+// }
+// err = ga.db.DeleteToken(ui)
+// if err != nil {
+// glog.Error(err)
+// }
+// fmt.Fprint(rw, "signed out")
+//}
+
+/*
+Called by server. Authenticates user with credentials that were given in the docker login command. If the token in the
+DB is expired, the OIDC access token is validated and, if possible, refreshed.
+*/
+func (ga *OIDCAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ err := ga.db.ValidateToken(user, password)
+ if err == ExpiredToken {
+ _, err = ga.validateServerToken(user)
+ if err != nil {
+ return false, nil, err
+ }
+ } else if err != nil {
+ return false, nil, err
+ }
+
+ v, err := ga.db.GetValue(user)
+ if err != nil || v == nil {
+ if err == nil {
+ err = errors.New("no db value, please sign out and sign in again")
+ }
+ return false, nil, err
+ }
+ return true, v.Labels, err
+}
+
+func (ga *OIDCAuth) Stop() {
+ err := ga.db.Close()
+ if err != nil {
+ glog.Info("Problems at closing the token DB")
+ } else {
+ glog.Info("Token DB closed")
+ }
+}
+
+func (ga *OIDCAuth) Name() string {
+ return "OpenID Connect"
+}
diff --git a/auth_server/authn/plugin_authn.go b/auth_server/authn/plugin_authn.go
new file mode 100644
index 00000000..cebf3230
--- /dev/null
+++ b/auth_server/authn/plugin_authn.go
@@ -0,0 +1,83 @@
+/*
+ Copyright 2019 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "fmt"
+ "plugin"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type PluginAuthnConfig struct {
+ PluginPath string `yaml:"plugin_path"`
+}
+
+func lookupAuthnSymbol(cfg *PluginAuthnConfig) (api.Authenticator, error) {
+ // load module
+ plug, err := plugin.Open(cfg.PluginPath)
+ if err != nil {
+ return nil, fmt.Errorf("error while loading authn plugin: %v", err)
+ }
+
+ // look up for Authn
+ symAuthen, err := plug.Lookup("Authn")
+ if err != nil {
+ return nil, fmt.Errorf("error while loading authn exporting the variable: %v", err)
+ }
+
+ // assert that loaded symbol is of a desired type
+ var authn api.Authenticator
+ authn, ok := symAuthen.(api.Authenticator)
+ if !ok {
+ return nil, fmt.Errorf("unexpected type from module symbol. Unable to cast Authn module")
+ }
+ return authn, nil
+}
+
+func (c *PluginAuthnConfig) Validate() error {
+ _, err := lookupAuthnSymbol(c)
+ return err
+}
+
+type PluginAuthn struct {
+ cfg *PluginAuthnConfig
+ Authn api.Authenticator
+}
+
+func (c *PluginAuthn) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ // use the plugin
+ return c.Authn.Authenticate(user, password)
+}
+
+func (c *PluginAuthn) Stop() {
+}
+
+func (c *PluginAuthn) Name() string {
+ return "plugin auth"
+}
+
+func NewPluginAuthn(cfg *PluginAuthnConfig) (*PluginAuthn, error) {
+ glog.Infof("Plugin authenticator: %s", cfg)
+ authn, err := lookupAuthnSymbol(cfg)
+ if err != nil {
+ return nil, err
+ }
+ return &PluginAuthn{Authn: authn}, nil
+}
diff --git a/auth_server/authn/static_auth.go b/auth_server/authn/static_auth.go
index 248f7304..4edc4892 100644
--- a/auth_server/authn/static_auth.go
+++ b/auth_server/authn/static_auth.go
@@ -19,10 +19,13 @@ package authn
import (
"encoding/json"
"golang.org/x/crypto/bcrypt"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
type Requirements struct {
- Password *PasswordString `yaml:"password,omitempty" json:"password,omitempty"`
+ Password *api.PasswordString `yaml:"password,omitempty" json:"password,omitempty"`
+ Labels api.Labels `yaml:"labels,omitempty" json:"labels,omitempty"`
}
type staticUsersAuth struct {
@@ -32,7 +35,7 @@ type staticUsersAuth struct {
func (r Requirements) String() string {
p := r.Password
if p != nil {
- pm := PasswordString("***")
+ pm := api.PasswordString("***")
r.Password = &pm
}
b, _ := json.Marshal(r)
@@ -44,17 +47,17 @@ func NewStaticUserAuth(users map[string]*Requirements) *staticUsersAuth {
return &staticUsersAuth{users: users}
}
-func (sua *staticUsersAuth) Authenticate(user string, password PasswordString) (bool, error) {
+func (sua *staticUsersAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
reqs := sua.users[user]
if reqs == nil {
- return false, NoMatch
+ return false, nil, api.NoMatch
}
if reqs.Password != nil {
if bcrypt.CompareHashAndPassword([]byte(*reqs.Password), []byte(password)) != nil {
- return false, nil
+ return false, nil, nil
}
}
- return true, nil
+ return true, reqs.Labels, nil
}
func (sua *staticUsersAuth) Stop() {
diff --git a/auth_server/authn/tokendb_gcs.go b/auth_server/authn/tokendb_gcs.go
new file mode 100644
index 00000000..53a0d278
--- /dev/null
+++ b/auth_server/authn/tokendb_gcs.go
@@ -0,0 +1,135 @@
+/*
+ Copyright 2017 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "cloud.google.com/go/storage"
+ "github.com/cesanta/glog"
+ "github.com/dchest/uniuri"
+ "golang.org/x/crypto/bcrypt"
+ "golang.org/x/net/context"
+ "google.golang.org/api/option"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type GCSStoreConfig struct {
+ Bucket string `yaml:"bucket,omitempty"`
+ ClientSecretFile string `yaml:"client_secret_file,omitempty"`
+ TokenHashCost int `yaml:"token_hash_cost,omitempty"`
+}
+
+// NewGCSTokenDB return a new TokenDB structure which uses Google Cloud Storage as backend. The
+// created DB uses file-per-user strategy and stores credentials independently for each user.
+//
+// Note: it's not recomanded bucket to be shared with other apps or services
+func NewGCSTokenDB(options *GCSStoreConfig) (TokenDB, error) {
+ gcs, err := storage.NewClient(context.Background(), option.WithServiceAccountFile(options.ClientSecretFile))
+ tokenHashCost := options.TokenHashCost
+ if tokenHashCost <= 0 {
+ tokenHashCost = bcrypt.DefaultCost
+ }
+ return &gcsTokenDB{gcs, options.Bucket, tokenHashCost}, err
+}
+
+type gcsTokenDB struct {
+ gcs *storage.Client
+ bucket string
+ tokenHashCost int
+}
+
+// GetValue gets token value associated with the provided user. Each user
+// in the bucket is having it's own file for tokens and it's recomanded bucket
+// to not be shared with other apps
+func (db *gcsTokenDB) GetValue(user string) (*TokenDBValue, error) {
+ rd, err := db.gcs.Bucket(db.bucket).Object(user).NewReader(context.Background())
+ if err == storage.ErrObjectNotExist {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("could not retrieved token for user '%s' due: %v", user, err)
+ }
+ defer rd.Close()
+
+ var dbv TokenDBValue
+ if err := json.NewDecoder(rd).Decode(&dbv); err != nil {
+ glog.Errorf("bad DB value for %q: %v", user, err)
+ return nil, fmt.Errorf("could not read token for user '%s' due: %v", user, err)
+ }
+
+ return &dbv, nil
+}
+
+// StoreToken stores token in the GCS file in a JSON format. Note that separate file is
+// used for each user
+func (db *gcsTokenDB) StoreToken(user string, v *TokenDBValue, updatePassword bool) (dp string, err error) {
+ if updatePassword {
+ dp = uniuri.New()
+ dph, _ := bcrypt.GenerateFromPassword([]byte(dp), db.tokenHashCost)
+ v.DockerPassword = string(dph)
+ }
+
+ wr := db.gcs.Bucket(db.bucket).Object(user).NewWriter(context.Background())
+
+ if err := json.NewEncoder(wr).Encode(v); err != nil {
+ glog.Errorf("failed to set token data for %s: %s", user, err)
+ return "", fmt.Errorf("failed to set token data for %s due: %v", user, err)
+ }
+
+ err = wr.Close()
+ return
+}
+
+// ValidateToken verifies whether the provided token passed as password field
+// is still valid, e.g available and not expired
+func (db *gcsTokenDB) ValidateToken(user string, password api.PasswordString) error {
+ dbv, err := db.GetValue(user)
+ if err != nil {
+ return err
+ }
+ if dbv == nil {
+ return api.NoMatch
+ }
+
+ if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
+ return api.WrongPass
+ }
+ if time.Now().After(dbv.ValidUntil) {
+ return ExpiredToken
+ }
+
+ return nil
+}
+
+// DeleteToken deletes the GCS file that is associated with the provided user.
+func (db *gcsTokenDB) DeleteToken(user string) error {
+ ctx := context.Background()
+ err := db.gcs.Bucket(db.bucket).Object(user).Delete(ctx)
+ if err == storage.ErrObjectNotExist {
+ return nil
+ }
+ return err
+}
+
+// Close is a nop operation for this db
+func (db *gcsTokenDB) Close() error {
+ return nil
+}
diff --git a/auth_server/authn/tokendb_level.go b/auth_server/authn/tokendb_level.go
new file mode 100644
index 00000000..66d43444
--- /dev/null
+++ b/auth_server/authn/tokendb_level.go
@@ -0,0 +1,158 @@
+/*
+ Copyright 2015 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/cesanta/glog"
+ "github.com/dchest/uniuri"
+ "github.com/syndtr/goleveldb/leveldb"
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+const (
+ tokenDBPrefix = "t:" // Keys in the database are t:email@example.com
+)
+
+var ExpiredToken = errors.New("expired token")
+
+type LevelDBStoreConfig struct {
+ Path string `yaml:"path,omitempty"`
+ TokenHashCost int `yaml:"token_hash_cost,omitempty"`
+}
+
+// TokenDB stores tokens using LevelDB
+type TokenDB interface {
+ // GetValue takes a username returns the corresponding token
+ GetValue(string) (*TokenDBValue, error)
+
+ // StoreToken takes a username and token, stores them in the DB
+ // and returns a password and error
+ StoreToken(string, *TokenDBValue, bool) (string, error)
+
+ // ValidateTOken takes a username and password
+ // and returns an error
+ ValidateToken(string, api.PasswordString) error
+
+ // DeleteToken takes a username
+ // and deletes the corresponding token from the DB
+ DeleteToken(string) error
+
+ // Composed from leveldb.DB
+ Close() error
+}
+
+// TokenDB stores tokens using LevelDB
+type TokenDBImpl struct {
+ *leveldb.DB
+}
+
+// TokenDBValue is stored in the database, JSON-serialized.
+type TokenDBValue struct {
+ TokenType string `json:"token_type,omitempty"` // Usually "Bearer"
+ AccessToken string `json:"access_token,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty"`
+ ValidUntil time.Time `json:"valid_until,omitempty"`
+ // DockerPassword is the temporary password we use to authenticate Docker users.
+ // Generated at the time of token creation, stored here as a BCrypt hash.
+ DockerPassword string `json:"docker_password,omitempty"`
+ Labels api.Labels `json:"labels,omitempty"`
+}
+
+// NewTokenDB returns a new TokenDB structure
+func NewTokenDB(options *LevelDBStoreConfig) (TokenDB, error) {
+ db, err := leveldb.OpenFile(options.Path, nil)
+ tokenHashCost := options.TokenHashCost
+ if tokenHashCost <= 0 {
+ tokenHashCost = bcrypt.DefaultCost
+ }
+ return &TokenDBImpl{
+ DB: db,
+ }, err
+}
+
+func (db *TokenDBImpl) GetValue(user string) (*TokenDBValue, error) {
+ valueStr, err := db.Get(getDBKey(user), nil)
+ switch {
+ case err == leveldb.ErrNotFound:
+ return nil, nil
+ case err != nil:
+ glog.Errorf("error accessing token db: %s", err)
+ return nil, fmt.Errorf("error accessing token db: %s", err)
+ }
+ var dbv TokenDBValue
+ err = json.Unmarshal(valueStr, &dbv)
+ if err != nil {
+ glog.Errorf("bad DB value for %q (%q): %s", user, string(valueStr), err)
+ return nil, fmt.Errorf("bad DB value due: %v", err)
+ }
+ return &dbv, nil
+}
+
+func (db *TokenDBImpl) StoreToken(user string, v *TokenDBValue, updatePassword bool) (dp string, err error) {
+ if updatePassword {
+ dp = uniuri.New()
+ dph, _ := bcrypt.GenerateFromPassword([]byte(dp), bcrypt.DefaultCost)
+ v.DockerPassword = string(dph)
+ }
+
+ data, err := json.Marshal(v)
+ if err != nil {
+ return "", err
+ }
+ err = db.Put(getDBKey(user), data, nil)
+ if err != nil {
+ glog.Errorf("failed to set token data for %s: %s", user, err)
+ }
+ glog.V(2).Infof("Server tokens for %s: %s", user, string(data))
+ return
+}
+
+func (db *TokenDBImpl) ValidateToken(user string, password api.PasswordString) error {
+ dbv, err := db.GetValue(user)
+ if err != nil {
+ return err
+ }
+ if dbv == nil {
+ return api.NoMatch
+ }
+ if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
+ return api.WrongPass
+ }
+ if time.Now().After(dbv.ValidUntil) {
+ return ExpiredToken
+ }
+ return nil
+}
+
+func (db *TokenDBImpl) DeleteToken(user string) error {
+ glog.V(1).Infof("deleting token for %s", user)
+ if err := db.Delete(getDBKey(user), nil); err != nil {
+ return fmt.Errorf("failed to delete %s: %s", user, err)
+ }
+ return nil
+}
+
+func getDBKey(user string) []byte {
+ return []byte(fmt.Sprintf("%s%s", tokenDBPrefix, user))
+}
diff --git a/auth_server/authn/tokendb_redis.go b/auth_server/authn/tokendb_redis.go
new file mode 100644
index 00000000..39a4f10a
--- /dev/null
+++ b/auth_server/authn/tokendb_redis.go
@@ -0,0 +1,160 @@
+/*
+ Copyright 2017 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+ "github.com/cesanta/glog"
+ "github.com/dchest/uniuri"
+ "github.com/go-redis/redis"
+)
+
+type RedisStoreConfig struct {
+ ClientOptions *redis.Options `yaml:"redis_options,omitempty"`
+ ClusterOptions *redis.ClusterOptions `yaml:"redis_cluster_options,omitempty"`
+ TokenHashCost int `yaml:"token_hash_cost,omitempty"`
+}
+
+type RedisClient interface {
+ Get(key string) *redis.StringCmd
+ Set(key string, value interface{}, expiration time.Duration) *redis.StatusCmd
+ Del(keys ...string) *redis.IntCmd
+}
+
+// NewRedisTokenDB returns a new TokenDB structure which uses Redis as the storage backend.
+//
+func NewRedisTokenDB(options *RedisStoreConfig) (TokenDB, error) {
+ var client RedisClient
+ if options.ClusterOptions != nil {
+ if options.ClientOptions != nil {
+ glog.Infof("Both redis_token_db.configs and redis_token_db.cluster_configs have been set. Only the latter will be used")
+ }
+ client = redis.NewClusterClient(options.ClusterOptions)
+ } else {
+ client = redis.NewClient(options.ClientOptions)
+ }
+ tokenHashCost := options.TokenHashCost
+ if tokenHashCost <= 0 {
+ tokenHashCost = bcrypt.DefaultCost
+ }
+
+ return &redisTokenDB{client,tokenHashCost}, nil
+}
+
+type redisTokenDB struct {
+ client RedisClient
+ tokenHashCost int
+}
+
+func (db *redisTokenDB) String() string {
+ return fmt.Sprintf("%v", db.client)
+}
+
+func (db *redisTokenDB) GetValue(user string) (*TokenDBValue, error) {
+ // Short-circuit calling Redis when the user is anonymous
+ if user == "" {
+ return nil, nil
+ }
+
+ key := string(getDBKey(user))
+
+ result, err := db.client.Get(key).Result()
+ if err == redis.Nil {
+ glog.V(2).Infof("Key <%s> doesn't exist\n", key)
+ return nil, nil
+ } else if err != nil {
+ glog.Errorf("Error getting Redis key <%s>: %s\n", key, err)
+ return nil, fmt.Errorf("Error getting key <%s>: %s", key, err)
+ }
+
+ var dbv TokenDBValue
+
+ err = json.Unmarshal([]byte(result), &dbv)
+ if err != nil {
+ glog.Errorf("Error parsing value for user <%q> (%q): %s", user, string(result), err)
+ return nil, fmt.Errorf("Error parsing value: %v", err)
+ }
+ glog.V(2).Infof("Redis: GET %s : %v\n", key, result)
+ return &dbv, nil
+}
+
+func (db *redisTokenDB) StoreToken(user string, v *TokenDBValue, updatePassword bool) (dp string, err error) {
+ if updatePassword {
+ dp = uniuri.New()
+ dph, _ := bcrypt.GenerateFromPassword([]byte(dp), db.tokenHashCost)
+ v.DockerPassword = string(dph)
+ }
+
+ data, err := json.Marshal(v)
+ if err != nil {
+ return "", err
+ }
+
+ key := string(getDBKey(user))
+
+ err = db.client.Set(key, data, 0).Err()
+ if err != nil {
+ glog.Errorf("Failed to store token data for user <%s>: %s\n", user, err)
+ return "", fmt.Errorf("Failed to store token data for user <%s>: %s", user, err)
+ }
+
+ glog.V(2).Infof("Server tokens for <%s>: %x\n", user, string(data))
+ return
+}
+
+func (db *redisTokenDB) ValidateToken(user string, password api.PasswordString) error {
+ dbv, err := db.GetValue(user)
+
+ if err != nil {
+ return err
+ }
+
+ if dbv == nil {
+ return api.NoMatch
+ }
+
+ if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
+ return api.WrongPass
+ }
+
+ if time.Now().After(dbv.ValidUntil) {
+ return ExpiredToken
+ }
+
+ return nil
+}
+
+func (db *redisTokenDB) DeleteToken(user string) error {
+ glog.Infof("Deleting token for user <%s>\n", user)
+
+ key := string(getDBKey(user))
+ err := db.client.Del(key).Err()
+ if err != nil {
+ return fmt.Errorf("Failed to delete token for user <%s>: %s", user, err)
+ }
+ return nil
+}
+
+func (db *redisTokenDB) Close() error {
+ return nil
+}
diff --git a/auth_server/authn/xorm_authn.go b/auth_server/authn/xorm_authn.go
new file mode 100644
index 00000000..34b2cb23
--- /dev/null
+++ b/auth_server/authn/xorm_authn.go
@@ -0,0 +1,96 @@
+/*
+ Copyright 2020 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ "fmt"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+ "golang.org/x/crypto/bcrypt"
+
+ _ "github.com/go-sql-driver/mysql"
+ _ "github.com/lib/pq"
+ "xorm.io/xorm"
+)
+
+var (
+ EnableSQLite3 = false
+)
+
+type XormAuthnConfig struct {
+ DatabaseType string `yaml:"database_type,omitempty"`
+ ConnString string `yaml:"conn_string,omitempty"`
+}
+
+type XormAuthn struct {
+ config *XormAuthnConfig
+ engine *xorm.Engine
+}
+
+type XormUser struct {
+ Id int64 `xorm:"pk autoincr"`
+ Username string `xorm:"VARCHAR(128) NOT NULL"`
+ PasswordHash string `xorm:"VARCHAR(128) NOT NULL"`
+ Labels api.Labels `xorm:"JSON"`
+}
+
+func NewXormAuth(c *XormAuthnConfig) (*XormAuthn, error) {
+ e, err := xorm.NewEngine(c.DatabaseType, c.ConnString)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := e.Sync2(new(XormUser)); err != nil {
+ return nil, fmt.Errorf("Sync2: %v", err)
+ }
+ return &XormAuthn{
+ config: c,
+ engine: e,
+ }, nil
+}
+
+func (xa *XormAuthn) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
+ if user == "" || password == "" {
+ return false, nil, api.NoMatch
+ }
+ var xuser XormUser
+ has, err := xa.engine.Where("username = ?", user).Desc("id").Get(&xuser)
+ if err != nil {
+ return false, nil, err
+ }
+ if !has {
+ return false, nil, api.NoMatch
+ }
+ if bcrypt.CompareHashAndPassword([]byte(xuser.PasswordHash), []byte(password)) != nil {
+ return false, nil, nil
+ }
+ return true, xuser.Labels, nil
+}
+
+func (xa *XormAuthn) Name() string {
+ return "XORM.io Authn"
+}
+
+func (xa *XormAuthn) Stop() {
+ if xa.engine != nil {
+ xa.engine.Close()
+ }
+}
+func (xa *XormAuthnConfig) Validate(configKey string) error {
+ // TODO: Validate auth
+ return nil
+}
diff --git a/auth_server/authn/xorm_sqlite_authn.go b/auth_server/authn/xorm_sqlite_authn.go
new file mode 100644
index 00000000..f1a39ccc
--- /dev/null
+++ b/auth_server/authn/xorm_sqlite_authn.go
@@ -0,0 +1,27 @@
+//+build sqlite
+
+/*
+ Copyright 2020 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authn
+
+import (
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func init() {
+ EnableSQLite3 = true
+}
diff --git a/auth_server/authz/acl.go b/auth_server/authz/acl.go
index b0576d32..b0aa21c7 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -1,12 +1,19 @@
package authz
import (
+ "context"
"encoding/json"
+ "fmt"
+ "net"
"path"
+ "reflect"
"regexp"
+ "strconv"
"strings"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
type ACL []ACLEntry
@@ -14,34 +21,111 @@ type ACL []ACLEntry
type ACLEntry struct {
Match *MatchConditions `yaml:"match"`
Actions *[]string `yaml:"actions,flow"`
+ Comment *string `yaml:"comment,omitempty"`
}
type MatchConditions struct {
- Account *string `yaml:"account,omitempty" json:"account,omitempty"`
- Type *string `yaml:"type,omitempty" json:"type,omitempty"`
- Name *string `yaml:"name,omitempty" json:"name,omitempty"`
+ Account *string `yaml:"account,omitempty" json:"account,omitempty"`
+ Type *string `yaml:"type,omitempty" json:"type,omitempty"`
+ Name *string `yaml:"name,omitempty" json:"name,omitempty"`
+ IP *string `yaml:"ip,omitempty" json:"ip,omitempty"`
+ Service *string `yaml:"service,omitempty" json:"service,omitempty"`
+ Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
}
type aclAuthorizer struct {
acl ACL
}
-func NewACLAuthorizer(acl ACL) Authorizer {
- return &aclAuthorizer{acl: acl}
+func validatePattern(p string) error {
+ if len(p) > 2 && p[0] == '/' && p[len(p)-1] == '/' {
+ _, err := regexp.Compile(p[1 : len(p)-1])
+ if err != nil {
+ return fmt.Errorf("invalid regex pattern: %s", err)
+ }
+ }
+ return nil
+}
+
+func parseIPPattern(ipp string) (*net.IPNet, error) {
+ ipnet := net.IPNet{}
+ ipnet.IP = net.ParseIP(ipp)
+ if ipnet.IP != nil {
+ if ipnet.IP.To4() != nil {
+ ipnet.Mask = net.CIDRMask(32, 32)
+ } else {
+ ipnet.Mask = net.CIDRMask(128, 128)
+ }
+ return &ipnet, nil
+ } else {
+ _, ipnet, err := net.ParseCIDR(ipp)
+ if err != nil {
+ return nil, err
+ }
+ return ipnet, nil
+ }
+}
+
+func validateMatchConditions(mc *MatchConditions) error {
+ for _, p := range []*string{mc.Account, mc.Type, mc.Name, mc.Service} {
+ if p == nil {
+ continue
+ }
+ err := validatePattern(*p)
+ if err != nil {
+ return fmt.Errorf("invalid pattern %q: %s", *p, err)
+ }
+ }
+ if mc.IP != nil {
+ _, err := parseIPPattern(*mc.IP)
+ if err != nil {
+ return fmt.Errorf("invalid IP pattern: %s", err)
+ }
+ }
+ for k, v := range mc.Labels {
+ err := validatePattern(v)
+ if err != nil {
+ return fmt.Errorf("invalid match pattern %q for label %s: %s", v, k, err)
+ }
+ }
+ return nil
}
-func (aa *aclAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
+func ValidateACL(acl ACL) error {
+ for i, e := range acl {
+ err := validateMatchConditions(e.Match)
+ if err != nil {
+ return fmt.Errorf("entry %d, invalid match conditions: %s", i, err)
+ }
+ }
+ return nil
+}
+
+// NewACLAuthorizer Creates a new static authorizer with ACL that have been read from the config file
+func NewACLAuthorizer(acl ACL) (api.Authorizer, error) {
+ if err := ValidateACL(acl); err != nil {
+ return nil, err
+ }
+ glog.V(1).Infof("Created ACL Authorizer with %d entries", len(acl))
+ return &aclAuthorizer{acl: acl}, nil
+}
+
+func (aa *aclAuthorizer) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
for _, e := range aa.acl {
matched := e.Matches(ai)
if matched {
- glog.V(2).Infof("%s matched %s", ai, e)
+ comment := "(nil)"
+ if e.Comment != nil {
+ comment = *e.Comment
+ }
+ glog.V(2).Infof("%s matched %s (Comment: %s)", ai, e, comment)
if len(*e.Actions) == 1 && (*e.Actions)[0] == "*" {
return ai.Actions, nil
}
return StringSetIntersection(ai.Actions, *e.Actions), nil
}
}
- return nil, NoMatch
+ return nil, api.NoMatch
}
func (aa *aclAuthorizer) Stop() {
@@ -75,17 +159,180 @@ func matchString(pp *string, s string, vars []string) bool {
return err == nil && matched
}
-func (e *ACLEntry) Matches(ai *AuthRequestInfo) bool {
+func matchStringWithLabelPermutations(pp *string, s string, vars []string, labelMap *map[string][]string) bool {
+ var matched bool
+ // First try basic matching
+ matched = matchString(pp, s, vars)
+ // If basic matching fails then try with label permuations
+ if !matched {
+ // Take the labelMap and build the structure required for the cartesian library
+ var labelSets [][]interface{}
+ for placeholder, labels := range *labelMap {
+ // Don't bother generating perumations for placeholders not in match string
+ // Since the label permuations are a cartesian product this can have
+ // a huge impact on performance
+ if strings.Contains(*pp, placeholder) {
+ var labelSet []interface{}
+ for _, label := range labels {
+ labelSet = append(labelSet, []string{placeholder, label})
+ }
+ labelSets = append(labelSets, labelSet)
+ }
+ }
+ if len(labelSets) > 0 {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ for permuation := range IterWithContext(ctx, labelSets...) {
+ var labelVars []string
+ for _, val := range permuation {
+ labelVars = append(labelVars, val.([]string)...)
+ }
+ matched = matchString(pp, s, append(vars, labelVars...))
+ if matched {
+ return matched
+ }
+ }
+ }
+ }
+ return matched
+}
+
+func IterWithContext(ctx context.Context, params ...[]interface{}) <-chan []interface{} {
+ c := make(chan []interface{})
+
+ if len(params) == 0 {
+ close(c)
+ return c
+ }
+
+ go func() {
+ defer close(c) // Ensure the channel is closed when the goroutine exits
+
+ iterate(ctx, c, params[0], []interface{}{}, params[1:]...)
+ }()
+
+ return c
+}
+
+func iterate(ctx context.Context, channel chan []interface{}, topLevel, result []interface{}, needUnpacking ...[]interface{}) {
+ if len(needUnpacking) == 0 {
+ for _, p := range topLevel {
+ select {
+ case <-ctx.Done():
+ return // Exit if the context is canceled
+ case channel <- append(append([]interface{}{}, result...), p):
+ }
+ }
+ return
+ }
+
+ for _, p := range topLevel {
+ select {
+ case <-ctx.Done():
+ return // Exit if the context is canceled
+ default:
+ iterate(ctx, channel, needUnpacking[0], append(result, p), needUnpacking[1:]...)
+ }
+ }
+}
+
+func matchIP(ipp *string, ip net.IP) bool {
+ if ipp == nil {
+ return true
+ }
+ if ip == nil {
+ return false
+ }
+ ipnet, err := parseIPPattern(*ipp)
+ if err != nil { // Can't happen, it supposed to have been validated
+ glog.Fatalf("Invalid IP pattern: %s", *ipp)
+ }
+ return ipnet.Contains(ip)
+}
+
+func matchLabels(ml map[string]string, rl api.Labels, vars []string) bool {
+ for label, pattern := range ml {
+ labelValues := rl[label]
+ matched := false
+ for _, lv := range labelValues {
+ if matchString(&pattern, lv, vars) {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return false
+ }
+ }
+ return true
+}
+
+var captureGroupRegex = regexp.MustCompile(`\$\{(.+?):(\d+)\}`)
+
+func getField(i interface{}, name string) (string, bool) {
+ s := reflect.Indirect(reflect.ValueOf(i))
+ f := reflect.Indirect(s.FieldByName(name))
+ if !f.IsValid() {
+ return "", false
+ }
+ return f.String(), true
+}
+
+func (mc *MatchConditions) Matches(ai *api.AuthRequestInfo) bool {
vars := []string{
"${account}", regexp.QuoteMeta(ai.Account),
"${type}", regexp.QuoteMeta(ai.Type),
"${name}", regexp.QuoteMeta(ai.Name),
"${service}", regexp.QuoteMeta(ai.Service),
}
- if matchString(e.Match.Account, ai.Account, vars) &&
- matchString(e.Match.Type, ai.Type, vars) &&
- matchString(e.Match.Name, ai.Name, vars) {
- return true
+ for _, x := range []string{"Account", "Type", "Name", "Service"} {
+ field, _ := getField(mc, x)
+ for _, found := range captureGroupRegex.FindAllStringSubmatch(field, -1) {
+ key := strings.Title(found[1])
+ index, _ := strconv.Atoi(found[2])
+ field, has := getField(mc, key)
+ if !has {
+ glog.Errorf("No field in '%s' in MatchConditions", key)
+ continue
+ }
+ if len(field) < 2 || field[0] != '/' || field[len(field)-1] != '/' {
+ continue
+ }
+ regex, err := regexp.Compile(field[1 : len(field)-1])
+ if err != nil {
+ glog.Errorf("Invalid regex in '%s' of MatchConditions", key)
+ continue
+ }
+ info, has := getField(ai, key)
+ if !has {
+ glog.Errorf("No field in '%s' in AuthRequestInfo", key)
+ continue
+ }
+ text := regex.FindStringSubmatch(info)
+ if index < 1 || index > len(text)-1 {
+ glog.Errorf("%s: Capture group index out of range", key)
+ continue
+ }
+ vars = append(vars, found[0], text[index])
+ }
}
- return false
+ labelMap := make(map[string][]string)
+ for label, labelValues := range ai.Labels {
+ var labelSet []string
+ for _, lv := range labelValues {
+ labelSet = append(labelSet, lv)
+ }
+ labelMap[fmt.Sprintf("${labels:%s}", label)] = labelSet
+ }
+ return matchStringWithLabelPermutations(mc.Account, ai.Account, vars, &labelMap) &&
+ matchStringWithLabelPermutations(mc.Type, ai.Type, vars, &labelMap) &&
+ matchStringWithLabelPermutations(mc.Name, ai.Name, vars, &labelMap) &&
+ matchStringWithLabelPermutations(mc.Service, ai.Service, vars, &labelMap) &&
+ matchIP(mc.IP, ai.IP) &&
+ matchLabels(mc.Labels, ai.Labels, vars)
+}
+
+func (e *ACLEntry) Matches(ai *api.AuthRequestInfo) bool {
+ return e.Match.Matches(ai)
}
diff --git a/auth_server/authz/acl_mongo.go b/auth_server/authz/acl_mongo.go
new file mode 100644
index 00000000..5439d482
--- /dev/null
+++ b/auth_server/authz/acl_mongo.go
@@ -0,0 +1,209 @@
+package authz
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "sync"
+ "time"
+
+ "github.com/cesanta/glog"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+ "gopkg.in/mgo.v2/bson"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+ "github.com/cesanta/docker_auth/auth_server/mgo_session"
+)
+
+type MongoACL []MongoACLEntry
+
+type MongoACLEntry struct {
+ ACLEntry `bson:",inline"`
+ Seq *int
+}
+
+type ACLMongoConfig struct {
+ MongoConfig *mgo_session.Config `yaml:"dial_info,omitempty"`
+ Collection string `yaml:"collection,omitempty"`
+ CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
+}
+
+type aclMongoAuthorizer struct {
+ lastCacheUpdate time.Time
+ lock sync.RWMutex
+ config *ACLMongoConfig
+ staticAuthorizer api.Authorizer
+ session *mongo.Client
+ context context.Context
+ updateTicker *time.Ticker
+ Collection string `yaml:"collection,omitempty"`
+ CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
+}
+
+// NewACLMongoAuthorizer creates a new ACL MongoDB authorizer
+func NewACLMongoAuthorizer(c *ACLMongoConfig) (api.Authorizer, error) {
+ // Attempt to create new MongoDB session.
+ session, err := mgo_session.New(c.MongoConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ authorizer := &aclMongoAuthorizer{
+ config: c,
+ session: session,
+ updateTicker: time.NewTicker(c.CacheTTL),
+ }
+
+ // Initially fetch the ACL from MongoDB
+ if err := authorizer.updateACLCache(); err != nil {
+ return nil, err
+ }
+
+ go authorizer.continuouslyUpdateACLCache()
+
+ return authorizer, nil
+}
+
+func (ma *aclMongoAuthorizer) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
+ ma.lock.RLock()
+ defer ma.lock.RUnlock()
+
+ // Test if authorizer has been initialized
+ if ma.staticAuthorizer == nil {
+ return nil, fmt.Errorf("MongoDB authorizer is not ready")
+ }
+
+ return ma.staticAuthorizer.Authorize(ai)
+}
+
+// Validate ensures that any custom config options
+// in a Config are set correctly.
+func (c *ACLMongoConfig) Validate(configKey string) error {
+ //First validate the MongoDB config.
+ if err := c.MongoConfig.Validate(configKey); err != nil {
+ return err
+ }
+
+ // Now check additional config fields.
+ if c.Collection == "" {
+ return fmt.Errorf("%s.collection is required", configKey)
+ }
+ if c.CacheTTL < 0 {
+ return fmt.Errorf("%s.cache_ttl is required (e.g. \"1m\" for 1 minute)", configKey)
+ }
+
+ return nil
+}
+
+func (ma *aclMongoAuthorizer) Stop() {
+ // This causes the background go routine which updates the ACL to stop
+ ma.updateTicker.Stop()
+
+ // Close connection to MongoDB database (if any)
+}
+
+func (ma *aclMongoAuthorizer) Name() string {
+ return "MongoDB ACL"
+}
+
+// continuouslyUpdateACLCache checks if the ACL cache has expired and depending
+// on the the result it updates the cache with the ACL from the MongoDB server.
+// The ACL will be stored inside the static authorizer instance which we use
+// to minimize duplication of code and maximize reuse of existing code.
+func (ma *aclMongoAuthorizer) continuouslyUpdateACLCache() {
+ var tick time.Time
+ for ; true; tick = <-ma.updateTicker.C {
+ aclAge := time.Now().Sub(ma.lastCacheUpdate)
+ glog.V(2).Infof("Updating ACL at %s (ACL age: %s. CacheTTL: %s)", tick, aclAge, ma.config.CacheTTL)
+
+ for true {
+ err := ma.updateACLCache()
+ if err == nil {
+ break
+ } else if err == io.EOF {
+ glog.Warningf("EOF error received from Mongo. Retrying connection")
+ time.Sleep(time.Second)
+ continue
+ } else {
+ glog.Errorf("Failed to update ACL. ERROR: %s", err)
+ glog.Warningf("Using stale ACL (Age: %s, TTL: %s)", aclAge, ma.config.CacheTTL)
+ break
+ }
+ }
+ }
+}
+
+func (ma *aclMongoAuthorizer) updateACLCache() error {
+ // Get ACL from MongoDB
+ var newACL MongoACL
+
+ collection := ma.session.Database(ma.config.MongoConfig.DialInfo.Database).Collection(ma.config.Collection)
+
+ // Create username index obj
+ index := mongo.IndexModel{
+ Keys: bson.M{"seq": 1},
+ Options: options.Index().SetUnique(true),
+ }
+
+ // Enforce a username index.
+ // mongodb will do no operation if index still exists.
+ // see: https://pkg.go.dev/go.mongodb.org/mongo-driver/mongo#Collection.Indexes
+ _, err := collection.Indexes().CreateOne(context.TODO(), index)
+ if err != nil {
+ fmt.Println(err.Error())
+ return err
+ }
+
+ // Get all ACLs that have the required key
+ cur, err := collection.Find(context.TODO(), bson.M{})
+
+ if err != nil {
+ return err
+ }
+
+ defer cur.Close(context.TODO())
+ for cur.Next(context.TODO()) {
+ var result MongoACLEntry
+ err := cur.Decode(&result) //Sort("seq")
+ if err != nil {
+ log.Fatal(err)
+ } else {
+ newACL = append(newACL, result)
+ }
+ // do something with result....
+ }
+ if err := cur.Err(); err != nil {
+ log.Fatal(err)
+ }
+
+ glog.V(2).Infof("Number of new ACL entries from MongoDB: %d", len(newACL))
+
+ // It is possible that the top document in the collection exists with a nil Seq.
+ // if that's true we pull it out of the slice and complain about it.
+ if len(newACL) > 0 && newACL[0].Seq == nil {
+ topACL := newACL[0]
+ return errors.New(fmt.Sprintf("Seq not set for ACL entry: %+v", topACL))
+ }
+
+ var retACL ACL
+ for _, e := range newACL {
+ retACL = append(retACL, e.ACLEntry)
+ }
+
+ newStaticAuthorizer, err := NewACLAuthorizer(retACL)
+ if err != nil {
+ return err
+ }
+
+ ma.lock.Lock()
+ ma.lastCacheUpdate = time.Now()
+ ma.staticAuthorizer = newStaticAuthorizer
+ ma.lock.Unlock()
+
+ glog.V(2).Infof("Got new ACL from MongoDB: %s", retACL)
+ glog.V(1).Infof("Installed new ACL from MongoDB (%d entries)", len(retACL))
+ return nil
+}
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
new file mode 100644
index 00000000..1b1d6745
--- /dev/null
+++ b/auth_server/authz/acl_test.go
@@ -0,0 +1,126 @@
+package authz
+
+import (
+ "net"
+ "testing"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+func sp(s string) *string {
+ return &s
+}
+
+func TestValidation(t *testing.T) {
+ cases := []struct {
+ mc MatchConditions
+ ok bool
+ }{
+ // Valid stuff
+ {MatchConditions{}, true},
+ {MatchConditions{Account: sp("foo")}, true},
+ {MatchConditions{Account: sp("foo?*")}, true},
+ {MatchConditions{Account: sp("/foo.*/")}, true},
+ {MatchConditions{Type: sp("foo")}, true},
+ {MatchConditions{Type: sp("foo?*")}, true},
+ {MatchConditions{Type: sp("/foo.*/")}, true},
+ {MatchConditions{Name: sp("foo")}, true},
+ {MatchConditions{Name: sp("foo?*")}, true},
+ {MatchConditions{Name: sp("/foo.*/")}, true},
+ {MatchConditions{Service: sp("foo")}, true},
+ {MatchConditions{Service: sp("foo?*")}, true},
+ {MatchConditions{Service: sp("/foo.*/")}, true},
+ {MatchConditions{IP: sp("192.168.0.1")}, true},
+ {MatchConditions{IP: sp("192.168.0.0/16")}, true},
+ {MatchConditions{IP: sp("2001:db8::1")}, true},
+ {MatchConditions{IP: sp("2001:db8::/48")}, true},
+ {MatchConditions{Labels: map[string]string{"foo": "bar"}}, true},
+ // Invalid stuff
+ {MatchConditions{Account: sp("/foo?*/")}, false},
+ {MatchConditions{Type: sp("/foo?*/")}, false},
+ {MatchConditions{Name: sp("/foo?*/")}, false},
+ {MatchConditions{Service: sp("/foo?*/")}, false},
+ {MatchConditions{IP: sp("192.168.0.1/100")}, false},
+ {MatchConditions{IP: sp("192.168.0.*")}, false},
+ {MatchConditions{IP: sp("foo")}, false},
+ {MatchConditions{IP: sp("2001:db8::/222")}, false},
+ {MatchConditions{Labels: map[string]string{"foo": "/bar?*/"}}, false},
+ }
+ for i, c := range cases {
+ result := validateMatchConditions(&c.mc)
+ if c.ok && result != nil {
+ t.Errorf("%d: %v: expected to pass, got %s", i, c.mc, result)
+ } else if !c.ok && result == nil {
+ t.Errorf("%d: %v: expected to fail, but it passed", i, c.mc)
+ }
+ }
+}
+
+func TestMatching(t *testing.T) {
+ ai1 := api.AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary"}
+ ai2 := api.AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
+ ai3 := api.AuthRequestInfo{Account: "foo", Type: "bar", Name: "admins/foo", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}}}
+ ai4 := api.AuthRequestInfo{Account: "foo", Type: "bar", Name: "VIP/api", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
+ ai5 := api.AuthRequestInfo{Account: "foo", Type: "bar", Name: "devs/api", Service: "notary",
+ Labels: map[string][]string{"group": []string{"admins", "VIP"}, "project": []string{"api", "frontend"}}}
+ cases := []struct {
+ mc MatchConditions
+ ai api.AuthRequestInfo
+ matches bool
+ }{
+ {MatchConditions{}, ai1, true},
+ {MatchConditions{Account: sp("foo")}, ai1, true},
+ {MatchConditions{Account: sp("foo"), Type: sp("bar")}, ai1, true},
+ {MatchConditions{Account: sp("foo"), Type: sp("baz")}, ai1, false},
+ {MatchConditions{Account: sp("fo?"), Type: sp("b*"), Name: sp("/z$/")}, ai1, true},
+ {MatchConditions{Account: sp("fo?"), Type: sp("b*"), Name: sp("/^z/")}, ai1, false},
+ {MatchConditions{Name: sp("${account}")}, api.AuthRequestInfo{Account: "foo", Name: "foo"}, true}, // Var subst
+ {MatchConditions{Name: sp("/${account}_.*/")}, api.AuthRequestInfo{Account: "foo", Name: "foo_x"}, true},
+ {MatchConditions{Name: sp("/${account}_.*/")}, api.AuthRequestInfo{Account: ".*", Name: "foo_x"}, false}, // Quoting
+ {MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:1}/*`)}, api.AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, true},
+ {MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:3}/*`)}, api.AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, false},
+ {MatchConditions{Account: sp(`/^(.+)@(.+?).test\.com$/`), Name: sp(`${account:1}-${account:2}/*`)}, api.AuthRequestInfo{Account: "john.smith@it.test.com", Name: "john.smith-it/test"}, true},
+ {MatchConditions{Service: sp("notary"), Type: sp("bar")}, ai1, true},
+ {MatchConditions{Service: sp("notary"), Type: sp("baz")}, ai1, false},
+ {MatchConditions{Service: sp("notary1"), Type: sp("bar")}, ai1, false},
+ // IP matching
+ {MatchConditions{IP: sp("127.0.0.1")}, api.AuthRequestInfo{IP: nil}, false},
+ {MatchConditions{IP: sp("127.0.0.1")}, api.AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
+ {MatchConditions{IP: sp("127.0.0.1")}, api.AuthRequestInfo{IP: net.IPv4(127, 0, 0, 2)}, false},
+ {MatchConditions{IP: sp("127.0.0.2")}, api.AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, false},
+ {MatchConditions{IP: sp("127.0.0.0/8")}, api.AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
+ {MatchConditions{IP: sp("127.0.0.0/8")}, api.AuthRequestInfo{IP: net.IPv4(127, 0, 0, 2)}, true},
+ {MatchConditions{IP: sp("2001:db8::1")}, api.AuthRequestInfo{IP: nil}, false},
+ {MatchConditions{IP: sp("2001:db8::1")}, api.AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
+ {MatchConditions{IP: sp("2001:db8::1")}, api.AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, false},
+ {MatchConditions{IP: sp("2001:db8::2")}, api.AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, false},
+ {MatchConditions{IP: sp("2001:db8::/48")}, api.AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
+ {MatchConditions{IP: sp("2001:db8::/48")}, api.AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, true},
+ // Label matching
+ {MatchConditions{Labels: map[string]string{"foo": "bar"}}, ai1, false},
+ {MatchConditions{Labels: map[string]string{"foo": "bar"}}, ai2, false},
+ {MatchConditions{Labels: map[string]string{"group": "admins"}}, ai2, true},
+ {MatchConditions{Labels: map[string]string{"foo": "bar", "group": "admins"}}, ai2, false}, // "and" logic
+ {MatchConditions{Labels: map[string]string{"group": "VIP"}}, ai2, true},
+ {MatchConditions{Labels: map[string]string{"group": "a*"}}, ai2, true},
+ {MatchConditions{Labels: map[string]string{"group": "/(admins|VIP)/"}}, ai2, true},
+ // // Label placeholder matching
+ {MatchConditions{Name: sp("${labels:group}/*")}, ai1, false}, // no labels
+ {MatchConditions{Name: sp("${labels:noexist}/*")}, ai2, false}, // wrong labels
+ {MatchConditions{Name: sp("${labels:group}/*")}, ai3, true}, // match label
+ {MatchConditions{Name: sp("${labels:noexist}/*")}, ai3, false}, // missing label
+ {MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
+ {MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail
+ {MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai4, true}, // multiple label match success
+ {MatchConditions{Name: sp("${labels:group}/${labels:noexist}")}, ai4, false}, // multiple label match fail wrong label
+ {MatchConditions{Name: sp("${labels:group}/${labels:project}")}, ai5, false}, // multiple label match fail. right label, wrong value
+ }
+ for i, c := range cases {
+ if result := c.mc.Matches(&c.ai); result != c.matches {
+ t.Errorf("%d: %#v vs %#v: expected %t, got %t", i, c.mc, c.ai, c.matches, result)
+ }
+ }
+}
diff --git a/auth_server/authz/acl_xorm.go b/auth_server/authz/acl_xorm.go
new file mode 100644
index 00000000..559b4bc7
--- /dev/null
+++ b/auth_server/authz/acl_xorm.go
@@ -0,0 +1,164 @@
+/*
+ Copyright 2020 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authz
+
+import (
+ "fmt"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+ "github.com/cesanta/glog"
+
+ _ "github.com/go-sql-driver/mysql"
+ _ "github.com/lib/pq"
+ "xorm.io/xorm"
+)
+
+var (
+ EnableSQLite3 = false
+)
+
+type XormAuthzConfig struct {
+ DatabaseType string `yaml:"database_type,omitempty"`
+ ConnString string `yaml:"conn_string,omitempty"`
+ CacheTTL time.Duration `yaml:"cache_ttl,omitempty"`
+}
+
+type XormACL []XormACLEntry
+
+type XormACLEntry struct {
+ ACLEntry `xorm:"'acl_entry' JSON"`
+ Seq int64
+}
+
+func (x XormACLEntry) TableName() string {
+ return "xorm_acl_entry"
+}
+
+type aclXormAuthz struct {
+ lastCacheUpdate time.Time
+ lock sync.RWMutex
+ config *XormAuthzConfig
+ staticAuthorizer api.Authorizer
+ engine *xorm.Engine
+ updateTicker *time.Ticker
+}
+
+func NewACLXormAuthz(c *XormAuthzConfig) (api.Authorizer, error) {
+ e, err := xorm.NewEngine(c.DatabaseType, c.ConnString)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := e.Sync2(new(XormACLEntry)); err != nil {
+ return nil, fmt.Errorf("Sync2: %v", err)
+ }
+ authorizer := &aclXormAuthz{
+ config: c,
+ engine: e,
+ updateTicker: time.NewTicker(c.CacheTTL),
+ }
+
+ // Initially fetch the ACL from XORM
+ if err := authorizer.updateACLCache(); err != nil {
+ return nil, err
+ }
+
+ go authorizer.continuouslyUpdateACLCache()
+
+ return authorizer, nil
+}
+
+func (xa *aclXormAuthz) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
+ xa.lock.RLock()
+ defer xa.lock.RUnlock()
+
+ // Test if authorizer has been initialized
+ if xa.staticAuthorizer == nil {
+ return nil, fmt.Errorf("XORM.io authorizer is not ready")
+ }
+
+ return xa.staticAuthorizer.Authorize(ai)
+}
+
+func (xa *aclXormAuthz) Stop() {
+ if xa.engine != nil {
+ xa.engine.Close()
+ }
+}
+func (xa *XormAuthzConfig) Validate(configKey string) error {
+ // TODO: Validate authz
+ return nil
+}
+
+func (xa *aclXormAuthz) Name() string {
+ return "XORM.io Authz"
+}
+
+func (xa *aclXormAuthz) continuouslyUpdateACLCache() {
+ var tick time.Time
+ for ; true; tick = <-xa.updateTicker.C {
+ aclAge := time.Now().Sub(xa.lastCacheUpdate)
+ glog.V(2).Infof("Updating ACL at %s (ACL age: %s. CacheTTL: %s)", tick, aclAge, xa.config.CacheTTL)
+
+ for true {
+ err := xa.updateACLCache()
+ if err == nil {
+ break
+ } else if err == io.EOF {
+ glog.Warningf("EOF error received from Xorm. Retrying connection")
+ time.Sleep(time.Second)
+ continue
+ } else {
+ glog.Errorf("Failed to update ACL. ERROR: %s", err)
+ glog.Warningf("Using stale ACL (Age: %s, TTL: %s)", aclAge, xa.config.CacheTTL)
+ break
+ }
+ }
+ }
+}
+
+func (xa *aclXormAuthz) updateACLCache() error {
+ // Get ACL from Xorm.io database connection
+ var newACL []XormACLEntry
+
+ err := xa.engine.OrderBy("seq").Find(&newACL)
+ if err != nil {
+ return err
+ }
+ var retACL ACL
+ for _, e := range newACL {
+ retACL = append(retACL, e.ACLEntry)
+ }
+
+ newStaticAuthorizer, err := NewACLAuthorizer(retACL)
+ if err != nil {
+ return err
+ }
+
+ xa.lock.Lock()
+ xa.lastCacheUpdate = time.Now()
+ xa.staticAuthorizer = newStaticAuthorizer
+ xa.lock.Unlock()
+
+ glog.V(2).Infof("Got new ACL from XORM: %s", retACL)
+ glog.V(1).Infof("Installed new ACL from XORM (%d entries)", len(retACL))
+ return nil
+
+}
diff --git a/auth_server/authz/acl_xorm_sqlite.go b/auth_server/authz/acl_xorm_sqlite.go
new file mode 100644
index 00000000..cdf5b81d
--- /dev/null
+++ b/auth_server/authz/acl_xorm_sqlite.go
@@ -0,0 +1,27 @@
+//+build sqlite
+
+/*
+ Copyright 2020 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authz
+
+import (
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func init() {
+ EnableSQLite3 = true
+}
diff --git a/auth_server/authz/casbin_authz.go b/auth_server/authz/casbin_authz.go
new file mode 100644
index 00000000..94ff0d8f
--- /dev/null
+++ b/auth_server/authz/casbin_authz.go
@@ -0,0 +1,116 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authz
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/casbin/casbin/v2"
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type CasbinAuthzConfig struct {
+ ModelFilePath string `yaml:"model_path"`
+ PolicyFilePath string `yaml:"policy_path"`
+}
+
+// labelMatch determines whether lbl1 matches lbl2.
+func labelMatch(lbl1 api.Labels, lbl2 api.Labels) bool {
+ for label := range lbl2 {
+ lbl1Values := lbl1[label]
+ lbl2Values := lbl2[label]
+
+ for _, val2 := range lbl2Values {
+ matched := false
+ for _, val1 := range lbl1Values {
+ if val1 == val2 {
+ matched = true
+ break
+ }
+ }
+
+ if !matched {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// labelMatchFunc is the wrapper for labelMatch.
+func labelMatchFunc(args ...interface{}) (interface{}, error) {
+ fmt.Println(args[0].(string))
+ lbl1 := stringToLabels(args[0].(string))
+ fmt.Println(labelsToString(lbl1))
+ lbl2 := stringToLabels(args[1].(string))
+ fmt.Println(lbl2)
+
+ return (bool)(labelMatch(lbl1, lbl2)), nil
+}
+
+func labelsToString(labels api.Labels) string {
+ labelsStr, err := json.Marshal(labels)
+ if err != nil {
+ return ""
+ }
+
+ return string(labelsStr)
+}
+
+func stringToLabels(str string) api.Labels {
+ labels := api.Labels{}
+ err := json.Unmarshal([]byte(str), &labels)
+ if err != nil {
+ return nil
+ }
+
+ return labels
+}
+
+type casbinAuthorizer struct {
+ enforcer *casbin.Enforcer
+ acl ACL
+}
+
+// NewCasbinAuthorizer creates a new casbin authorizer.
+func NewCasbinAuthorizer(enforcer *casbin.Enforcer) (api.Authorizer, error) {
+ enforcer.AddFunction("labelMatch", labelMatchFunc)
+ return &casbinAuthorizer{enforcer: enforcer}, nil
+}
+
+// Authorize determines whether to allow the actions.
+func (a *casbinAuthorizer) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
+ actions := []string{}
+
+ for _, action := range ai.Actions {
+ if ok, _ := a.enforcer.Enforce(ai.Account, ai.Type, ai.Name, ai.Service, ai.IP.String(), action, labelsToString(ai.Labels)); ok {
+ actions = append(actions, action)
+ }
+ }
+ return actions, nil
+
+ // return nil, NoMatch
+}
+
+// Stop stops the middleware.
+func (a *casbinAuthorizer) Stop() {
+ // Nothing to do.
+}
+
+// Name returns the name of the middleware.
+func (a *casbinAuthorizer) Name() string {
+ return "Casbin Authorizer"
+}
diff --git a/auth_server/authz/casbin_authz_test.go b/auth_server/authz/casbin_authz_test.go
new file mode 100644
index 00000000..106d8054
--- /dev/null
+++ b/auth_server/authz/casbin_authz_test.go
@@ -0,0 +1,115 @@
+// Copyright 2021 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package authz
+
+import (
+ "fmt"
+ "net"
+ "strings"
+ "testing"
+
+ "github.com/casbin/casbin/v2"
+ "github.com/casbin/casbin/v2/util"
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+func requestToString(ai api.AuthRequestInfo) string {
+ return fmt.Sprintf("{%s | %s | %s | %s | %s | %s | %s}", ai.Account, ai.Type, ai.Name, ai.Service, ai.IP.String(), strings.Join(ai.Actions, ","), labelsToString(ai.Labels))
+}
+
+func testRequest(t *testing.T, a api.Authorizer, account string, typ string, name string, service string, ip string, labels map[string][]string, actions []string, res []string) {
+ ai := api.AuthRequestInfo{
+ Account: account,
+ Type: typ,
+ Name: name,
+ Service: service,
+ IP: net.ParseIP(ip),
+ Actions: actions,
+ Labels: labels}
+
+ actions, err := a.Authorize(&ai)
+ if err != nil {
+ t.Error("Casbin authorizer fails to authorize.")
+ return
+ }
+
+ if !util.ArrayEquals(actions, res) {
+ t.Errorf("%s: %s, supposed to be %s", requestToString(ai), actions, res)
+ }
+}
+
+func TestLabelsToString(t *testing.T) {
+ label := map[string][]string{"a": {"b", "c"}, "d": {"e"}}
+ labelStr := labelsToString(label)
+ if labelStr != "{\"a\":[\"b\",\"c\"],\"d\":[\"e\"]}" {
+ t.Errorf("%s: %s, supposed to be %s", label, labelStr, "{\"a\":[\"b\",\"c\"],\"d\":[\"e\"]}")
+ }
+
+ labelNew := stringToLabels(labelStr)
+ if !labelMatch(label, labelNew) {
+ t.Errorf("%s: %s, supposed to be %s", label, labelNew, label)
+ }
+}
+
+func testLabels(t *testing.T, lbl1 api.Labels, lbl2 api.Labels, res bool) {
+ myRes := labelMatch(lbl1, lbl2)
+ if myRes != res {
+ t.Errorf("%s matches %s: %v, supposed to be %v", lbl1, lbl2, myRes, res)
+ }
+}
+
+func TestLabels(t *testing.T) {
+ testLabels(t, map[string][]string{"a": {"b"}}, map[string][]string{"a": {"b"}}, true)
+ testLabels(t, map[string][]string{"a": {"b"}}, map[string][]string{"a": {"c"}}, false)
+ testLabels(t, map[string][]string{"a": {"b", "c"}}, map[string][]string{"a": {"b"}}, true)
+ testLabels(t, map[string][]string{"a": {"b"}}, map[string][]string{"a": {"b", "c"}}, false)
+ testLabels(t, map[string][]string{"a": {"b", "c"}, "d": {"e"}}, map[string][]string{"a": {"b", "c"}}, true)
+ testLabels(t, map[string][]string{"a": {"b"}}, map[string][]string{"a": {"b", "c"}, "d": {"f"}}, false)
+}
+
+func TestPermissions(t *testing.T) {
+ e, err := casbin.NewEnforcer("../../examples/casbin_authz_model.conf",
+ "../../examples/casbin_authz_policy.csv")
+ if err != nil {
+ t.Errorf("Enforcer fails to create: %v", err)
+ }
+ a, err := NewCasbinAuthorizer(e)
+ if err != nil {
+ t.Error("Casbin authorizer fails to create.")
+ }
+
+ // alice is a user.
+ testRequest(t, a, "alice", "book", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{"write", "read"})
+ testRequest(t, a, "alice", "book", "book1", "bookstore1", "1.2.3.3", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{})
+ testRequest(t, a, "alice", "book", "book2", "bookstore2", "1.2.3.4", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{})
+ testRequest(t, a, "alice", "pen", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{})
+ testRequest(t, a, "alice", "book", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"c"}}, []string{"write", "read", "delete"}, []string{})
+ testRequest(t, a, "alice", "book", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"b", "c"}}, []string{"write", "read", "delete"}, []string{"write", "read"})
+
+ // bob is a member of role1, so bob will have all permissions of role1.
+ testRequest(t, a, "bob", "book", "book2", "bookstore1", "192.168.1.123", map[string][]string{"a": {"b", "c"}, "d": {"e"}}, []string{"write", "read", "delete"}, []string{"read"})
+ testRequest(t, a, "bob", "book", "book2", "bookstore1", "192.168.1.123", map[string][]string{"a": {"b"}, "d": {"e"}}, []string{"write", "read", "delete"}, []string{})
+ testRequest(t, a, "bob", "book", "book2", "bookstore1", "192.168.0.123", map[string][]string{"a": {"b", "c"}, "d": {"e"}}, []string{"write", "read", "delete"}, []string{})
+ testRequest(t, a, "bob", "book", "book2", "bookstore1", "192.168.1.123", map[string][]string{"a": {"b", "c"}}, []string{"write", "read", "delete"}, []string{"read"})
+ testRequest(t, a, "bob", "book", "book2", "restaurant", "192.168.1.123", map[string][]string{"a": {"b", "c"}, "d": {"e"}}, []string{"write", "read", "delete"}, []string{})
+
+ // admin is the administrator, so he can do anything without restriction.
+ testRequest(t, a, "admin", "book", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{"write", "read", "delete"})
+ testRequest(t, a, "admin", "book", "book1", "bookstore1", "1.2.3.3", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{"write", "read", "delete"})
+ testRequest(t, a, "admin", "book", "book2", "bookstore2", "1.2.3.4", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{"write", "read", "delete"})
+ testRequest(t, a, "admin", "pen", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"b"}}, []string{"write", "read", "delete"}, []string{"write", "read", "delete"})
+ testRequest(t, a, "admin", "book", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"c"}}, []string{"write", "read", "delete"}, []string{"write", "read", "delete"})
+ testRequest(t, a, "admin", "book", "book1", "bookstore1", "1.2.3.4", map[string][]string{"a": {"b", "c"}}, []string{"write", "read", "delete"}, []string{"write", "read", "delete"})
+}
diff --git a/auth_server/authz/ext_authz.go b/auth_server/authz/ext_authz.go
new file mode 100644
index 00000000..0e270b22
--- /dev/null
+++ b/auth_server/authz/ext_authz.go
@@ -0,0 +1,101 @@
+/*
+ Copyright 2016 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authz
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strings"
+ "syscall"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type ExtAuthzConfig struct {
+ Command string `yaml:"command"`
+ Args []string `yaml:"args"`
+}
+
+type ExtAuthzStatus int
+
+const (
+ ExtAuthzAllowed ExtAuthzStatus = 0
+ ExtAuthzDenied ExtAuthzStatus = 1
+ ExtAuthzError ExtAuthzStatus = 2
+)
+
+func (c *ExtAuthzConfig) Validate() error {
+ if c.Command == "" {
+ return fmt.Errorf("command is not set")
+ }
+ if _, err := exec.LookPath(c.Command); err != nil {
+ return fmt.Errorf("invalid command %q: %s", c.Command, err)
+ }
+ return nil
+}
+
+type ExtAuthz struct {
+ cfg *ExtAuthzConfig
+}
+
+func NewExtAuthzAuthorizer(cfg *ExtAuthzConfig) *ExtAuthz {
+ glog.Infof("External authorization: %s %s", cfg.Command, strings.Join(cfg.Args, " "))
+ return &ExtAuthz{cfg: cfg}
+}
+
+func (ea *ExtAuthz) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
+ aiMarshal, err := json.Marshal(ai)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to json.Marshal AuthRequestInfo: %s", err)
+ }
+
+ cmd := exec.Command(ea.cfg.Command, ea.cfg.Args...)
+ cmd.Stdin = strings.NewReader(fmt.Sprintf("%s", aiMarshal))
+ output, err := cmd.Output()
+
+ es := 0
+ et := ""
+ if err == nil {
+ } else if ee, ok := err.(*exec.ExitError); ok {
+ es = ee.Sys().(syscall.WaitStatus).ExitStatus()
+ et = string(ee.Stderr)
+ } else {
+ es = int(ExtAuthzError)
+ et = fmt.Sprintf("cmd run error: %s", err)
+ }
+ glog.V(2).Infof("%s %s -> %d %s", cmd.Path, cmd.Args, es, output)
+
+ switch ExtAuthzStatus(es) {
+ case ExtAuthzAllowed:
+ return ai.Actions, nil
+ case ExtAuthzDenied:
+ return []string{}, nil
+ default:
+ glog.Errorf("Ext command error: %d %s", es, et)
+ }
+ return nil, fmt.Errorf("bad return code from command: %d", es)
+}
+
+func (sua *ExtAuthz) Stop() {
+}
+
+func (sua *ExtAuthz) Name() string {
+ return "external authz"
+}
diff --git a/auth_server/authz/plugin_authz.go b/auth_server/authz/plugin_authz.go
new file mode 100644
index 00000000..29909bca
--- /dev/null
+++ b/auth_server/authz/plugin_authz.go
@@ -0,0 +1,82 @@
+/*
+ Copyright 2019 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package authz
+
+import (
+ "fmt"
+ "plugin"
+
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+)
+
+type PluginAuthzConfig struct {
+ PluginPath string `yaml:"plugin_path"`
+}
+
+func lookupAuthzSymbol(cfg *PluginAuthzConfig) (api.Authorizer, error) {
+ // load module
+ plug, err := plugin.Open(cfg.PluginPath)
+ if err != nil {
+ return nil, fmt.Errorf("error while loading authz plugin: %v", err)
+ }
+
+ // look up for Authz
+ symAuthen, err := plug.Lookup("Authz")
+ if err != nil {
+ return nil, fmt.Errorf("error while loading authz exporting the variable: %v", err)
+ }
+
+ // assert that loaded symbol is of a desired type
+ var authz api.Authorizer
+ authz, ok := symAuthen.(api.Authorizer)
+ if !ok {
+ return nil, fmt.Errorf("unexpected type from module symbol. Unable to cast Authz module")
+ }
+ return authz, nil
+}
+
+func (c *PluginAuthzConfig) Validate() error {
+ _, err := lookupAuthzSymbol(c)
+ return err
+}
+
+type PluginAuthz struct {
+ Authz api.Authorizer
+}
+
+func (c *PluginAuthz) Stop() {
+}
+
+func (c *PluginAuthz) Name() string {
+ return "plugin authz"
+}
+
+func NewPluginAuthzAuthorizer(cfg *PluginAuthzConfig) (*PluginAuthz, error) {
+ glog.Infof("Plugin authorization: %s", cfg)
+ authz, err := lookupAuthzSymbol(cfg)
+ if err != nil {
+ return nil, err
+ }
+ return &PluginAuthz{Authz: authz}, nil
+}
+
+func (c *PluginAuthz) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
+ // use the plugin
+ return c.Authz.Authorize(ai)
+}
diff --git a/auth_server/gen_version.go b/auth_server/gen_version.go
new file mode 100644
index 00000000..65c86bda
--- /dev/null
+++ b/auth_server/gen_version.go
@@ -0,0 +1,99 @@
+//+build ignore
+
+/*
+ Copyright 2021 Cesanta Software Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/cooldrip/cstrftime" // strftime implemented with cgo
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+func main() {
+ dir, err := os.Getwd()
+ if err != nil {
+ log.Fatal(err)
+ }
+ r, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{DetectDotGit: true})
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ t := time.Now()
+ ts := cstrftime.Format("%Y%m%d-%H%M%S", t)
+
+ head, err := r.Head()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ short := fmt.Sprintf("%s", head.Hash())[:8]
+
+ w, err := r.Worktree()
+ if err != nil {
+ log.Fatal(err)
+ }
+ status, err := w.Status()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ is_dirty := ""
+ if len(status) > 0 {
+ is_dirty = "+"
+ }
+
+ branch_or_tag := head.Name().Short()
+ if branch_or_tag == "HEAD" {
+ branch_or_tag = "?"
+ }
+
+ tags, _ := r.Tags()
+ tags.ForEach(func(ref *plumbing.Reference) error {
+ if ref.Type() != plumbing.HashReference {
+ return nil
+ }
+
+ if strings.HasPrefix(ref.String(), short) {
+ tag := ref.String()
+ branch_or_tag = trimRef(strings.Split(tag, " ")[1])
+ }
+ return nil
+ })
+
+ buildId := fmt.Sprintf("%s/%s@%s%s", ts, branch_or_tag, short, is_dirty)
+
+ version := cstrftime.Format("%Y%m%d%H", t)
+ if is_dirty != "" || branch_or_tag == "?" {
+ version = branch_or_tag
+ }
+
+ fmt.Printf("%s\t%s\n", version, buildId)
+}
+
+func trimRef(ref string) string {
+ ref = strings.TrimPrefix(ref, "refs/heads/")
+ ref = strings.TrimPrefix(ref, "refs/tags/")
+ return ref
+}
diff --git a/auth_server/go.mod b/auth_server/go.mod
new file mode 100644
index 00000000..2a245dc6
--- /dev/null
+++ b/auth_server/go.mod
@@ -0,0 +1,83 @@
+module github.com/cesanta/docker_auth/auth_server
+
+go 1.23.0
+
+require (
+ cloud.google.com/go/storage v1.29.0
+ github.com/casbin/casbin/v2 v2.55.1
+ github.com/cesanta/glog v0.0.0-20150527111657-22eb27a0ae19
+ github.com/coreos/go-oidc/v3 v3.9.0
+ github.com/dchest/uniuri v0.0.0-20220929095258-3027df40b6ce
+ github.com/deckarep/golang-set v1.8.0
+ github.com/docker/distribution v2.8.2-beta.1+incompatible
+ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7
+ github.com/go-ldap/ldap v3.0.3+incompatible
+ github.com/go-redis/redis v6.15.9+incompatible
+ github.com/go-sql-driver/mysql v1.6.0
+ github.com/lib/pq v1.10.7
+ github.com/mattn/go-sqlite3 v2.0.3+incompatible
+ github.com/syndtr/goleveldb v1.0.0
+ go.mongodb.org/mongo-driver v1.10.2
+ golang.org/x/crypto v0.36.0
+ golang.org/x/net v0.38.0
+ golang.org/x/oauth2 v0.13.0
+ google.golang.org/api v0.126.0
+ gopkg.in/fsnotify.v1 v1.4.7
+ gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
+ gopkg.in/yaml.v2 v2.4.0
+ xorm.io/xorm v1.3.2
+)
+
+require (
+ cloud.google.com/go v0.110.2 // indirect
+ cloud.google.com/go/compute v1.20.1 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/iam v0.13.0 // indirect
+ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
+ github.com/go-jose/go-jose/v3 v3.0.4 // indirect
+ github.com/goccy/go-json v0.9.11 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/mock v1.6.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/s2a-go v0.1.4 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
+ github.com/googleapis/gax-go/v2 v2.11.0 // indirect
+ github.com/gorilla/mux v1.8.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.15.11 // indirect
+ github.com/kr/pretty v0.3.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/montanaflynn/stats v0.6.6 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rogpeppe/go-internal v1.9.0 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.1 // indirect
+ github.com/xdg-go/stringprep v1.0.3 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
+ go.opencensus.io v0.24.0 // indirect
+ golang.org/x/sync v0.12.0 // indirect
+ golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
+ google.golang.org/grpc v1.56.3 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
+ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ lukechampine.com/uint128 v1.2.0 // indirect
+ modernc.org/cc/v3 v3.36.3 // indirect
+ modernc.org/ccgo/v3 v3.16.9 // indirect
+ modernc.org/libc v1.17.1 // indirect
+ modernc.org/opt v0.1.3 // indirect
+ modernc.org/sqlite v1.18.1 // indirect
+ modernc.org/strutil v1.1.3 // indirect
+ xorm.io/builder v0.3.12 // indirect
+)
diff --git a/auth_server/go.sum b/auth_server/go.sum
new file mode 100644
index 00000000..e956374b
--- /dev/null
+++ b/auth_server/go.sum
@@ -0,0 +1,873 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
+cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
+cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
+cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
+cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
+cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
+gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
+gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
+gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/casbin/casbin/v2 v2.55.1 h1:vaTAHSLkQfielg9UiHdIdvIVK/NAmMjBkDkrOM9iDqI=
+github.com/casbin/casbin/v2 v2.55.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cesanta/glog v0.0.0-20150527111657-22eb27a0ae19 h1:qkZ2PnuOWrlzVJ4NO4PzkHyV6yHuUcRRsyrvhtU0HsU=
+github.com/cesanta/glog v0.0.0-20150527111657-22eb27a0ae19/go.mod h1:2z0CC6W/LJ/Tyhj0UuWExb1JmxhBTeujw3wU1JSM1Ps=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
+github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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/dchest/uniuri v0.0.0-20220929095258-3027df40b6ce h1:uHEdbpdf5QdO6Koxr10tA+p85JPbLHzwG3+uGysF0WY=
+github.com/dchest/uniuri v0.0.0-20220929095258-3027df40b6ce/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
+github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
+github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
+github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/docker/distribution v2.8.2-beta.1+incompatible h1:gILO60VLD2v28ozemv4aAwDb8ds5U2O/vD/sBXbd7Rw=
+github.com/docker/distribution v2.8.2-beta.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
+github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
+github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
+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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
+github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
+github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
+github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
+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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
+github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
+github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/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=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+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/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
+github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
+github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
+github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
+github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
+github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
+github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
+github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
+github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
+github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
+github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
+github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
+github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
+github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
+github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
+github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
+github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
+github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
+github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
+github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
+github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
+github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
+github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
+github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.mongodb.org/mongo-driver v1.10.2 h1:4Wk3cnqOrQCn0P92L3/mmurMxzdvWWs5J9jinAVKD+k=
+go.mongodb.org/mongo-driver v1.10.2/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8=
+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
+google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=
+google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
+google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
+google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
+modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg=
+modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
+modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
+modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
+modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
+modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
+modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
+modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
+modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
+modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
+modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
+modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
+modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
+modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
+modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
+modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
+modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
+modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
+modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
+modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
+modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
+modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
+modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
+modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
+modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
+modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
+modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
+modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
+modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
+modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
+modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
+modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
+modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
+modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
+modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
+modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
+modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
+modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM=
+modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
+modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
+modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
+modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
+modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
+modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
+modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
+modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
+modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
+modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
+modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
+modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
+modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
+modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
+modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
+modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
+modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
+modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
+modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
+modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
+modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
+modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
+modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
+modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
+modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
+modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
+modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
+modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
+modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
+modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
+modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
+modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
+modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
+modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
+modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
+modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
+modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
+modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
+modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
+modernc.org/libc v1.17.1 h1:Q8/Cpi36V/QBfuQaFVeisEBs3WqoGAJprZzmf7TfEYI=
+modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
+modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
+modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
+modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
+modernc.org/memory v1.2.1 h1:dkRh86wgmq/bJu2cAS2oqBCz/KsMZU7TUM4CibQ7eBs=
+modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
+modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8=
+modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
+modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
+modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
+modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
+modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
+modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
+xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
+xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
diff --git a/auth_server/main.go b/auth_server/main.go
index 34c6bab0..9a229de0 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -17,82 +17,167 @@
package main
import (
+ "context"
"crypto/tls"
"flag"
"math/rand"
+ "net"
"net/http"
"os"
"os/signal"
+ "strconv"
"syscall"
"time"
- "github.com/cesanta/docker_auth/auth_server/server"
- "github.com/facebookgo/httpdown"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
+ "golang.org/x/crypto/acme/autocert"
fsnotify "gopkg.in/fsnotify.v1"
+
+ "github.com/cesanta/docker_auth/auth_server/server"
+)
+
+var (
+ // Version comment
+ Version = ""
+ // BuildID comment
+ BuildID = ""
)
type RestartableServer struct {
configFile string
- hd *httpdown.HTTP
authServer *server.AuthServer
- hs httpdown.Server
+ hs *http.Server
}
-func ServeOnce(c *server.Config, cf string, hd *httpdown.HTTP) (*server.AuthServer, httpdown.Server) {
- glog.Infof("Config from %s (%d users, %d ACL entries)", cf, len(c.Users), len(c.ACL))
+func stringToUint16(s string) uint16 {
+ v, err := strconv.ParseUint(s, 0, 16)
+ if err != nil {
+ glog.Exitf("Failed to convert %s to uint16", s)
+ }
+ return uint16(v)
+}
+
+func ServeOnce(c *server.Config, cf string) (*server.AuthServer, *http.Server) {
+ glog.Infof("Config from %s (%d users, %d ACL static entries)", cf, len(c.Users), len(c.ACL))
as, err := server.NewAuthServer(c)
if err != nil {
glog.Exitf("Failed to create auth server: %s", err)
}
- var tlsConfig *tls.Config
+ tlsConfig := &tls.Config{}
+ if c.Server.HSTS {
+ glog.Info("HTTP Strict Transport Security enabled")
+ }
+ if c.Server.TLSMinVersion != "" {
+ value, found := server.TLSVersionValues[c.Server.TLSMinVersion]
+ if !found {
+ value = stringToUint16(c.Server.TLSMinVersion)
+ }
+ tlsConfig.MinVersion = value
+ glog.Infof("TLS MinVersion: %s", c.Server.TLSMinVersion)
+ }
+ if c.Server.TLSCurvePreferences != nil {
+ var values []tls.CurveID
+ for _, s := range c.Server.TLSCurvePreferences {
+ value, found := server.TLSCurveIDValues[s]
+ if !found {
+ value = tls.CurveID(stringToUint16(s))
+ }
+ values = append(values, value)
+ }
+ tlsConfig.CurvePreferences = values
+ glog.Infof("TLS CurvePreferences: %s", c.Server.TLSCurvePreferences)
+ }
+ if c.Server.TLSCipherSuites != nil {
+ var values []uint16
+ for _, s := range c.Server.TLSCipherSuites {
+ value, found := server.TLSCipherSuitesValues[s]
+ if !found {
+ value = stringToUint16(s)
+ }
+ values = append(values, value)
+ }
+ tlsConfig.CipherSuites = values
+ glog.Infof("TLS CipherSuites: %s", c.Server.TLSCipherSuites)
+ } else {
+ for _, s := range tls.CipherSuites() {
+ tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, s.ID)
+ }
+ }
if c.Server.CertFile != "" || c.Server.KeyFile != "" {
// Check for partial configuration.
if c.Server.CertFile == "" || c.Server.KeyFile == "" {
glog.Exitf("Failed to load certificate and key: both were not provided")
}
- tlsConfig = &tls.Config{
- MinVersion: tls.VersionTLS10,
- PreferServerCipherSuites: true,
- CipherSuites: []uint16{
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- tls.TLS_RSA_WITH_AES_128_CBC_SHA,
- tls.TLS_RSA_WITH_AES_256_CBC_SHA,
- },
- NextProtos: []string{"http/1.1"},
- Certificates: make([]tls.Certificate, 1),
- }
glog.Infof("Cert file: %s", c.Server.CertFile)
glog.Infof("Key file : %s", c.Server.KeyFile)
+ tlsConfig.Certificates = make([]tls.Certificate, 1)
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(c.Server.CertFile, c.Server.KeyFile)
if err != nil {
glog.Exitf("Failed to load certificate and key: %s", err)
}
+ } else if c.Server.LetsEncrypt.Email != "" {
+ m := &autocert.Manager{
+ Email: c.Server.LetsEncrypt.Email,
+ Cache: autocert.DirCache(c.Server.LetsEncrypt.CacheDir),
+ Prompt: autocert.AcceptTOS,
+ }
+ if c.Server.LetsEncrypt.Host != "" {
+ m.HostPolicy = autocert.HostWhitelist(c.Server.LetsEncrypt.Host)
+ }
+ glog.Infof("Using LetsEncrypt, host %q, email %q", c.Server.LetsEncrypt.Host, c.Server.LetsEncrypt.Email)
+ tlsConfig.GetCertificate = m.GetCertificate
} else {
glog.Warning("Running without TLS")
+ tlsConfig = nil
}
+
hs := &http.Server{
Addr: c.Server.ListenAddress,
Handler: as,
TLSConfig: tlsConfig,
}
- s, err := hd.ListenAndServe(hs)
- if err != nil {
- glog.Exitf("Failed to set up listener: %s", err)
+ var listener net.Listener
+ if c.Server.Net == "unix" {
+ // Remove socket, if exists
+ if _, err := os.Stat(c.Server.ListenAddress); err == nil {
+ if err := os.Remove(c.Server.ListenAddress); err != nil {
+ glog.Fatal(err.Error())
+ }
+ }
+ listener, err = net.Listen("unix", c.Server.ListenAddress)
+ if err != nil {
+ glog.Fatal(err.Error())
+ }
+ } else {
+ listener, err = net.Listen("tcp", c.Server.ListenAddress)
+ if err != nil {
+ glog.Fatal(err.Error())
+ }
}
- glog.Infof("Serving")
- return as, s
+
+ go func() {
+ if c.Server.CertFile == "" && c.Server.KeyFile == "" {
+ if err := hs.Serve(listener); err != nil {
+ if err == http.ErrServerClosed {
+ return
+ }
+ }
+ } else {
+ if err := hs.ServeTLS(listener, c.Server.CertFile, c.Server.KeyFile); err != nil {
+ if err == http.ErrServerClosed {
+ return
+ }
+ }
+ }
+ }()
+ glog.Infof("Serving on %s", c.Server.ListenAddress)
+ return as, hs
}
func (rs *RestartableServer) Serve(c *server.Config) {
- rs.authServer, rs.hs = ServeOnce(c, rs.configFile, rs.hd)
+ rs.authServer, rs.hs = ServeOnce(c, rs.configFile)
rs.WatchConfig()
}
@@ -101,6 +186,7 @@ func (rs *RestartableServer) WatchConfig() {
if err != nil {
glog.Fatalf("Failed to create watcher: %s", err)
}
+ defer w.Close()
stopSignals := make(chan os.Signal, 1)
signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT)
@@ -132,25 +218,26 @@ func (rs *RestartableServer) WatchConfig() {
case s := <-stopSignals:
signal.Stop(stopSignals)
glog.Infof("Signal: %s", s)
- rs.hs.Stop()
+ if err := rs.hs.Shutdown(context.Background()); err != nil {
+ glog.Errorf("HTTP server Shutdown: %v", err)
+ }
rs.authServer.Stop()
glog.Exitf("Exiting")
}
}
- w.Close()
}
func (rs *RestartableServer) MaybeRestart() {
- glog.Infof("Restarting server")
+ glog.Infof("Validating new config")
c, err := server.LoadConfig(rs.configFile)
if err != nil {
glog.Errorf("Failed to reload config (server not restarted): %s", err)
return
}
- glog.Infof("New config loaded")
- rs.hs.Stop()
+ glog.Infof("Config ok, restarting server")
+ rs.hs.Close()
rs.authServer.Stop()
- rs.authServer, rs.hs = ServeOnce(c, rs.configFile, rs.hd)
+ rs.authServer, rs.hs = ServeOnce(c, rs.configFile)
}
func main() {
@@ -158,17 +245,18 @@ func main() {
rand.Seed(time.Now().UnixNano())
glog.CopyStandardLogTo("INFO")
+ glog.Infof("docker_auth %s build %s", Version, BuildID)
+
cf := flag.Arg(0)
if cf == "" {
glog.Exitf("Config file not specified")
}
- c, err := server.LoadConfig(cf)
+ config, err := server.LoadConfig(cf)
if err != nil {
glog.Exitf("Failed to load config: %s", err)
}
rs := RestartableServer{
configFile: cf,
- hd: &httpdown.HTTP{},
}
- rs.Serve(c)
+ rs.Serve(config)
}
diff --git a/auth_server/mgo_session/mgo_session.go b/auth_server/mgo_session/mgo_session.go
new file mode 100644
index 00000000..3fa253a2
--- /dev/null
+++ b/auth_server/mgo_session/mgo_session.go
@@ -0,0 +1,165 @@
+/*
+ Copyright 2015 Cesanta Software Ltmc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliemc.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package mgo_session
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/cesanta/glog"
+
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type ServerAddr struct {
+ // contains filtered or unexported fields
+}
+
+type DialInfo struct {
+ // Addrs holds the addresses for the seed servers.
+ Addrs []string
+
+ // Direct informs whether to establish connections only with the
+ // specified seed servers, or to obtain information for the whole
+ // cluster and establish connections with further servers too.
+ Direct bool
+
+ // Timeout is the amount of time to wait for a server to respond when
+ // first connecting and on follow up operations in the session. If
+ // timeout is zero, the call may block forever waiting for a connection
+ // to be established.
+ Timeout time.Duration
+
+ // FailFast will cause connection and query attempts to fail faster when
+ // the server is unavailable, instead of retrying until the configured
+ // timeout period. Note that an unavailable server may silently drop
+ // packets instead of rejecting them, in which case it's impossible to
+ // distinguish it from a slow server, so the timeout stays relevant.
+ FailFast bool
+
+ // Database is the default database name used when the Session.DB method
+ // is called with an empty name, and is also used during the intial
+ // authenticatoin if Source is unset.
+ Database string
+
+ // Source is the database used to establish credentials and privileges
+ // with a MongoDB server. Defaults to the value of Database, if that is
+ // set, or "admin" otherwise.
+ Source string
+
+ // Service defines the service name to use when authenticating with the GSSAPI
+ // mechanism. Defaults to "mongodb".
+ Service string
+
+ // Mechanism defines the protocol for credential negotiation.
+ // Defaults to "MONGODB-CR".
+ Mechanism string
+
+ // Username and Password inform the credentials for the initial authentication
+ // done on the database defined by the Source field. See Session.Login.
+ Username string
+ Password string
+
+ // DialServer optionally specifies the dial function for establishing
+ // connections with the MongoDB servers.
+ DialServer func(addr *ServerAddr) (net.Conn, error)
+
+ // WARNING: This field is obsolete. See DialServer above.
+ Dial func(addr net.Addr) (net.Conn, error)
+}
+
+// Config stores how to connect to the MongoDB server and an optional password file
+type Config struct {
+ DialInfo DialInfo `yaml:",inline"`
+
+ PasswordFile string `yaml:"password_file,omitempty"`
+ EnableTLS bool `yaml:"enable_tls,omitempty"`
+}
+
+// Validate ensures the most common fields inside the mgo.DialInfo portion of
+// a Config are set correctly as well as other fields inside the
+// Config itself.
+func (c *Config) Validate(configKey string) error {
+ if len(c.DialInfo.Addrs) == 0 {
+ return fmt.Errorf("At least one element in %s.dial_info.addrs is required", configKey)
+ }
+ if c.DialInfo.Timeout == 0 {
+ c.DialInfo.Timeout = 10 * time.Second
+ }
+ if c.DialInfo.Database == "" {
+ return fmt.Errorf("%s.dial_info.database is required", configKey)
+ }
+ return nil
+}
+
+var retClient *mongo.Client = nil
+
+func New(c *Config) (*mongo.Client, error) {
+
+ if nil == retClient {
+ // Attempt to create a MongoDB session which we can re-use when handling
+ // multiple requests. We can optionally read in the password from a file or directly from the config.
+
+ // Read in the password (if any)
+ if c.PasswordFile != "" {
+ passBuf, err := ioutil.ReadFile(c.PasswordFile)
+ if err != nil {
+ return nil, fmt.Errorf(`Failed to read password file "%s": %s`, c.PasswordFile, err)
+ }
+ c.DialInfo.Password = strings.TrimSpace(string(passBuf))
+ }
+
+ glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.Timeout)
+
+ session, err := DialWithInfo(&c.DialInfo, c.EnableTLS)
+ retClient = session
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return retClient, nil
+}
+
+func DialWithInfo(info *DialInfo, enableTLS bool) (*mongo.Client, error) {
+
+ sslActivationString := "ssl=false"
+ if enableTLS {
+ sslActivationString = "ssl=true"
+ }
+
+ // Connect
+ username := url.QueryEscape(info.Username)
+ password := url.QueryEscape(info.Password)
+ uri := "mongodb://" + username + ":" + password + "@" + info.Addrs[0] + "/?authSource=admin&" + sslActivationString
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
+ if err != nil {
+ panic(err)
+ } else {
+ fmt.Println("Successfully connected!")
+ }
+ return client, err
+}
diff --git a/auth_server/server/config.go b/auth_server/server/config.go
index aa4fae61..13c610b7 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -17,60 +17,181 @@
package server
import (
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/sha256"
"crypto/tls"
"crypto/x509"
+ "encoding/base64"
"errors"
"fmt"
"io/ioutil"
+ "math/big"
+ "os"
"strings"
+ "time"
- "github.com/cesanta/docker_auth/auth_server/authn"
- "github.com/cesanta/docker_auth/auth_server/authz"
"github.com/docker/libtrust"
yaml "gopkg.in/yaml.v2"
+
+ "github.com/cesanta/docker_auth/auth_server/authn"
+ "github.com/cesanta/docker_auth/auth_server/authz"
)
type Config struct {
- Server ServerConfig `yaml:"server"`
- Token TokenConfig `yaml:"token"`
- Users map[string]*authn.Requirements `yaml:"users,omitempty"`
- GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
- LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
- ACL authz.ACL `yaml:"acl"`
+ Server ServerConfig `yaml:"server"`
+ Token TokenConfig `yaml:"token"`
+ Users map[string]*authn.Requirements `yaml:"users,omitempty"`
+ GoogleAuth *authn.GoogleAuthConfig `yaml:"google_auth,omitempty"`
+ GitHubAuth *authn.GitHubAuthConfig `yaml:"github_auth,omitempty"`
+ OIDCAuth *authn.OIDCAuthConfig `yaml:"oidc_auth,omitempty"`
+ GitlabAuth *authn.GitlabAuthConfig `yaml:"gitlab_auth,omitempty"`
+ LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
+ MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
+ XormAuthn *authn.XormAuthnConfig `yaml:"xorm_auth,omitempty"`
+ ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
+ PluginAuthn *authn.PluginAuthnConfig `yaml:"plugin_authn,omitempty"`
+ ACL authz.ACL `yaml:"acl,omitempty"`
+ ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
+ ACLXorm *authz.XormAuthzConfig `yaml:"acl_xorm,omitempty"`
+ ExtAuthz *authz.ExtAuthzConfig `yaml:"ext_authz,omitempty"`
+ PluginAuthz *authz.PluginAuthzConfig `yaml:"plugin_authz,omitempty"`
+ CasbinAuthz *authz.CasbinAuthzConfig `yaml:"casbin_authz,omitempty"`
}
type ServerConfig struct {
- ListenAddress string `yaml:"addr,omitempty"`
- CertFile string `yaml:"certificate,omitempty"`
- KeyFile string `yaml:"key,omitempty"`
+ ListenAddress string `yaml:"addr,omitempty"`
+ Net string `yaml:"net,omitempty"`
+ PathPrefix string `yaml:"path_prefix,omitempty"`
+ RealIPHeader string `yaml:"real_ip_header,omitempty"`
+ RealIPPos int `yaml:"real_ip_pos,omitempty"`
+ CertFile string `yaml:"certificate,omitempty"`
+ KeyFile string `yaml:"key,omitempty"`
+ HSTS bool `yaml:"hsts,omitempty"`
+ TLSMinVersion string `yaml:"tls_min_version,omitempty"`
+ TLSCurvePreferences []string `yaml:"tls_curve_preferences,omitempty"`
+ TLSCipherSuites []string `yaml:"tls_cipher_suites,omitempty"`
+ LetsEncrypt LetsEncryptConfig `yaml:"letsencrypt,omitempty"`
publicKey libtrust.PublicKey
privateKey libtrust.PrivateKey
+ sigAlg string
+}
+
+type LetsEncryptConfig struct {
+ Host string `yaml:"host,omitempty"`
+ Email string `yaml:"email,omitempty"`
+ CacheDir string `yaml:"cache_dir,omitempty"`
}
type TokenConfig struct {
- Issuer string `yaml:"issuer,omitempty"`
- CertFile string `yaml:"certificate,omitempty"`
- KeyFile string `yaml:"key,omitempty"`
- Expiration int64 `yaml:"expiration,omitempty"`
+ Issuer string `yaml:"issuer,omitempty"`
+ CertFile string `yaml:"certificate,omitempty"`
+ KeyFile string `yaml:"key,omitempty"`
+ Expiration int64 `yaml:"expiration,omitempty"`
+ DisableLegacyKeyID bool `yaml:"disable_legacy_key_id,omitempty"`
publicKey libtrust.PublicKey
privateKey libtrust.PrivateKey
+ sigAlg string
+ keyID string
+}
+
+// TLSCipherSuitesValues maps CipherSuite names as strings to the actual values
+// in the crypto/tls package
+// Taken from https://golang.org/pkg/crypto/tls/#pkg-constants
+var TLSCipherSuitesValues = map[string]uint16{
+ // TLS 1.0 - 1.2 cipher suites.
+ "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
+ "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+ "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+ "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+ "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
+ "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+ "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+ "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+ "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+ "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+ "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+ // TLS 1.3 cipher suites.
+ "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
+ "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
+ "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
+ // TLS_FALLBACK_SCSV isn't a standard cipher suite but an indicator
+ // that the client is doing version fallback. See RFC 7507.
+ "TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
+}
+
+// TLSVersionValues maps Version names as strings to the actual values in the
+// crypto/tls package
+// Taken from https://golang.org/pkg/crypto/tls/#pkg-constants
+var TLSVersionValues = map[string]uint16{
+ "TLS10": tls.VersionTLS10,
+ "TLS11": tls.VersionTLS11,
+ "TLS12": tls.VersionTLS12,
+ "TLS13": tls.VersionTLS13,
+ // Deprecated: SSLv3 is cryptographically broken, and will be
+ // removed in Go 1.14. See golang.org/issue/32716.
+ "SSL30": tls.VersionSSL30,
+}
+
+// TLSCurveIDValues maps CurveID names as strings to the actual values in the
+// crypto/tls package
+// Taken from https://golang.org/pkg/crypto/tls/#CurveID
+var TLSCurveIDValues = map[string]tls.CurveID{
+ "P256": tls.CurveP256,
+ "P384": tls.CurveP384,
+ "P521": tls.CurveP521,
+ "X25519": tls.X25519,
}
func validate(c *Config) error {
if c.Server.ListenAddress == "" {
return errors.New("server.addr is required")
}
-
+ if c.Server.Net != "unix" && c.Server.Net != "tcp" {
+ if c.Server.Net == "" {
+ c.Server.Net = "tcp"
+ } else {
+ return errors.New("server.net must be unix or tcp")
+ }
+ }
+ if c.Server.PathPrefix != "" && !strings.HasPrefix(c.Server.PathPrefix, "/") {
+ return errors.New("server.path_prefix must be an absolute path")
+ }
+ if (c.Server.TLSMinVersion == "0x0304" || c.Server.TLSMinVersion == "TLS13") && c.Server.TLSCipherSuites != nil {
+ return errors.New("TLS 1.3 ciphersuites are not configurable")
+ }
if c.Token.Issuer == "" {
return errors.New("token.issuer is required")
}
if c.Token.Expiration <= 0 {
return fmt.Errorf("expiration must be positive, got %d", c.Token.Expiration)
}
- if c.Users == nil && c.GoogleAuth == nil && c.LDAPAuth == nil {
- return errors.New("no auth methods are configured, this is probably a mistake. Use an empty user map if you really want to deny everyone.")
+ if c.Users == nil && c.ExtAuth == nil && c.GoogleAuth == nil && c.GitHubAuth == nil && c.GitlabAuth == nil && c.OIDCAuth == nil && c.LDAPAuth == nil && c.MongoAuth == nil && c.XormAuthn == nil && c.PluginAuthn == nil {
+ return errors.New("no auth methods are configured, this is probably a mistake. Use an empty user map if you really want to deny everyone")
+ }
+ if c.MongoAuth != nil {
+ if err := c.MongoAuth.Validate("mongo_auth"); err != nil {
+ return err
+ }
+ }
+ if c.XormAuthn != nil {
+ if err := c.XormAuthn.Validate("xorm_auth"); err != nil {
+ return err
+ }
}
if gac := c.GoogleAuth; gac != nil {
if gac.ClientSecretFile != "" {
@@ -80,20 +201,151 @@ func validate(c *Config) error {
}
gac.ClientSecret = strings.TrimSpace(string(contents))
}
- if gac.ClientId == "" || gac.ClientSecret == "" || gac.TokenDB == "" {
- return errors.New("google_auth.{client_id,client_secret,token_db} are required.")
+ if gac.ClientId == "" || gac.ClientSecret == "" || (gac.LevelTokenDB != nil && gac.LevelTokenDB.Path == "") {
+ return errors.New("google_auth.{client_id,client_secret,level_token_db.path} are required")
+ }
+
+ if gac.ClientId == "" || gac.ClientSecret == "" || (gac.GCSTokenDB != nil && (gac.GCSTokenDB.Bucket == "" || gac.GCSTokenDB.ClientSecretFile == "")) {
+ return errors.New("google_auth.{client_id,client_secret,gcs_token_db{bucket,client_secret_file}} are required")
+ }
+
+ if gac.ClientId == "" || gac.ClientSecret == "" || (gac.RedisTokenDB != nil && gac.RedisTokenDB.ClientOptions == nil && gac.RedisTokenDB.ClusterOptions == nil) {
+ return errors.New("google_auth.{client_id,client_secret,redis_token_db.{redis_options,redis_cluster_options}} are required")
}
+
if gac.HTTPTimeout <= 0 {
- gac.HTTPTimeout = 10
+ gac.HTTPTimeout = time.Duration(10 * time.Second)
+ }
+ }
+ if ghac := c.GitHubAuth; ghac != nil {
+ if ghac.ClientSecretFile != "" {
+ contents, err := ioutil.ReadFile(ghac.ClientSecretFile)
+ if err != nil {
+ return fmt.Errorf("could not read %s: %s", ghac.ClientSecretFile, err)
+ }
+ ghac.ClientSecret = strings.TrimSpace(string(contents))
+ }
+ if ghac.ClientId == "" || ghac.ClientSecret == "" || (ghac.LevelTokenDB != nil && ghac.LevelTokenDB.Path == "") {
+ return errors.New("github_auth.{client_id,client_secret,level_token_db.path} are required")
+ }
+
+ if ghac.ClientId == "" || ghac.ClientSecret == "" || (ghac.GCSTokenDB != nil && (ghac.GCSTokenDB.Bucket == "" || ghac.GCSTokenDB.ClientSecretFile == "")) {
+ return errors.New("github_auth.{client_id,client_secret,gcs_token_db{bucket,client_secret_file}} are required")
+ }
+
+ if ghac.ClientId == "" || ghac.ClientSecret == "" || (ghac.RedisTokenDB != nil && ghac.RedisTokenDB.ClientOptions == nil && ghac.RedisTokenDB.ClusterOptions == nil) {
+ return errors.New("github_auth.{client_id,client_secret,redis_token_db.{redis_options,redis_cluster_options}} are required")
+ }
+
+ if ghac.HTTPTimeout <= 0 {
+ ghac.HTTPTimeout = time.Duration(10 * time.Second)
+ }
+ if ghac.RevalidateAfter == 0 {
+ // Token expires after 1 hour by default
+ ghac.RevalidateAfter = time.Duration(1 * time.Hour)
+ }
+ }
+ if oidc := c.OIDCAuth; oidc != nil {
+ if oidc.ClientSecretFile != "" {
+ contents, err := ioutil.ReadFile(oidc.ClientSecretFile)
+ if err != nil {
+ return fmt.Errorf("could not read %s: %s", oidc.ClientSecretFile, err)
+ }
+ oidc.ClientSecret = strings.TrimSpace(string(contents))
+ }
+ if oidc.ClientId == "" || oidc.ClientSecret == "" || oidc.Issuer == "" || oidc.RedirectURL == "" || (oidc.LevelTokenDB != nil && oidc.LevelTokenDB.Path == "") {
+ return errors.New("oidc_auth.{issuer,redirect_url,client_id,client_secret,level_token_db.path} are required")
+ }
+
+ if oidc.ClientId == "" || oidc.ClientSecret == "" || (oidc.GCSTokenDB != nil && (oidc.GCSTokenDB.Bucket == "" || oidc.GCSTokenDB.ClientSecretFile == "")) {
+ return errors.New("oidc_auth.{client_id,client_secret,gcs_token_db{bucket,client_secret_file}} are required")
+ }
+
+ if oidc.ClientId == "" || oidc.ClientSecret == "" || (oidc.RedisTokenDB != nil && oidc.RedisTokenDB.ClientOptions == nil && oidc.RedisTokenDB.ClusterOptions == nil) {
+ return errors.New("oidc_auth.{client_id,client_secret,redis_token_db.{redis_options,redis_cluster_options}} are required")
+ }
+
+ if oidc.HTTPTimeout <= 0 {
+ oidc.HTTPTimeout = time.Duration(10 * time.Second)
+ }
+ if oidc.UserClaim == "" {
+ oidc.UserClaim = "email"
+ }
+ if oidc.Scopes == nil {
+ oidc.Scopes = []string{"openid", "email"}
+ }
+ }
+ if glab := c.GitlabAuth; glab != nil {
+ if glab.ClientSecretFile != "" {
+ contents, err := ioutil.ReadFile(glab.ClientSecretFile)
+ if err != nil {
+ return fmt.Errorf("could not read %s: %s", glab.ClientSecretFile, err)
+ }
+ glab.ClientSecret = strings.TrimSpace(string(contents))
+ }
+ if glab.ClientId == "" || glab.ClientSecret == "" || (glab.LevelTokenDB != nil && glab.LevelTokenDB.Path == "") {
+ return errors.New("gitlab_auth.{client_id,client_secret,level_token_db.path} are required")
+ }
+
+ if glab.ClientId == "" || glab.ClientSecret == "" || (glab.GCSTokenDB != nil && (glab.GCSTokenDB.Bucket == "" || glab.GCSTokenDB.ClientSecretFile == "")) {
+ return errors.New("gitlab_auth.{client_id,client_secret,gcs_token_db{bucket,client_secret_file}} are required")
+ }
+
+ if glab.ClientId == "" || glab.ClientSecret == "" || (glab.RedisTokenDB != nil && glab.RedisTokenDB.ClientOptions == nil && glab.RedisTokenDB.ClusterOptions == nil) {
+ return errors.New("gitlab_auth.{client_id,client_secret,redis_token_db.{redis_options,redis_cluster_options}} are required")
+ }
+
+ if glab.HTTPTimeout <= 0 {
+ glab.HTTPTimeout = time.Duration(10 * time.Second)
+ }
+ if glab.RevalidateAfter == 0 {
+ // Token expires after 1 hour by default
+ glab.RevalidateAfter = time.Duration(1 * time.Hour)
+ }
+ }
+ if c.ExtAuth != nil {
+ if err := c.ExtAuth.Validate(); err != nil {
+ return fmt.Errorf("bad ext_auth config: %s", err)
+ }
+ }
+ if c.ACL == nil && c.ACLXorm == nil && c.ACLMongo == nil && c.ExtAuthz == nil && c.PluginAuthz == nil {
+ return errors.New("ACL is empty, this is probably a mistake. Use an empty list if you really want to deny all actions")
+ }
+
+ if c.ACL != nil {
+ if err := authz.ValidateACL(c.ACL); err != nil {
+ return fmt.Errorf("invalid ACL: %s", err)
+ }
+ }
+ if c.ACLMongo != nil {
+ if err := c.ACLMongo.Validate("acl_mongo"); err != nil {
+ return err
}
}
- if c.ACL == nil {
- return errors.New("ACL is empty, this is probably a mistake. Use an empty list if you really want to deny all actions.")
+ if c.ACLXorm != nil {
+ if err := c.ACLXorm.Validate("acl_xorm"); err != nil {
+ return err
+ }
+ }
+ if c.ExtAuthz != nil {
+ if err := c.ExtAuthz.Validate(); err != nil {
+ return err
+ }
+ }
+ if c.PluginAuthn != nil {
+ if err := c.PluginAuthn.Validate(); err != nil {
+ return fmt.Errorf("bad plugin_authn config: %s", err)
+ }
+ }
+ if c.PluginAuthz != nil {
+ if err := c.PluginAuthz.Validate(); err != nil {
+ return fmt.Errorf("bad plugin_authz config: %s", err)
+ }
}
return nil
}
-func loadCertAndKey(certFile, keyFile string) (pk libtrust.PublicKey, prk libtrust.PrivateKey, err error) {
+func loadCertAndKey(certFile string, keyFile string) (pk libtrust.PublicKey, prk libtrust.PrivateKey, sigAlg string, err error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return
@@ -107,6 +359,11 @@ func loadCertAndKey(certFile, keyFile string) (pk libtrust.PublicKey, prk libtru
return
}
prk, err = libtrust.FromCryptoPrivateKey(cert.PrivateKey)
+ _, sigAlg, errStr := prk.Sign(strings.NewReader("dummy"), 0)
+ if errStr != nil {
+ err = fmt.Errorf("failed to sign: %s", errStr)
+ return
+ }
return
}
@@ -128,7 +385,7 @@ func LoadConfig(fileName string) (*Config, error) {
if c.Server.CertFile == "" || c.Server.KeyFile == "" {
return nil, fmt.Errorf("failed to load server cert and key: both were not provided")
}
- c.Server.publicKey, c.Server.privateKey, err = loadCertAndKey(c.Server.CertFile, c.Server.KeyFile)
+ c.Server.publicKey, c.Server.privateKey, c.Server.sigAlg, err = loadCertAndKey(c.Server.CertFile, c.Server.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load server cert and key: %s", err)
}
@@ -140,7 +397,7 @@ func LoadConfig(fileName string) (*Config, error) {
if c.Token.CertFile == "" || c.Token.KeyFile == "" {
return nil, fmt.Errorf("failed to load token cert and key: both were not provided")
}
- c.Token.publicKey, c.Token.privateKey, err = loadCertAndKey(c.Token.CertFile, c.Token.KeyFile)
+ c.Token.publicKey, c.Token.privateKey, c.Token.sigAlg, err = loadCertAndKey(c.Token.CertFile, c.Token.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load token cert and key: %s", err)
}
@@ -148,12 +405,60 @@ func LoadConfig(fileName string) (*Config, error) {
}
if serverConfigured && !tokenConfigured {
- c.Token.publicKey, c.Token.privateKey = c.Server.publicKey, c.Server.privateKey
+ c.Token.publicKey, c.Token.privateKey, c.Token.sigAlg = c.Server.publicKey, c.Server.privateKey, c.Server.sigAlg
tokenConfigured = true
}
if !tokenConfigured {
return nil, fmt.Errorf("failed to load token cert and key: none provided")
}
+
+ if c.Token.DisableLegacyKeyID {
+ c.Token.keyID = getRFC7638Thumbprint(c.Token.publicKey.CryptoPublicKey())
+ } else {
+ c.Token.keyID = c.Token.publicKey.KeyID()
+ }
+
+ if !serverConfigured && c.Server.LetsEncrypt.Email != "" {
+ if c.Server.LetsEncrypt.CacheDir == "" {
+ return nil, fmt.Errorf("server.letsencrypt.cache_dir is required")
+ }
+ // We require that LetsEncrypt is an existing directory, because we really don't want it
+ // to be misconfigured and obtained certificates to be lost.
+ fi, err := os.Stat(c.Server.LetsEncrypt.CacheDir)
+ if err != nil || !fi.IsDir() {
+ return nil, fmt.Errorf("server.letsencrypt.cache_dir (%s) does not exist or is not a directory", c.Server.LetsEncrypt.CacheDir)
+ }
+ }
+
return c, nil
}
+
+// getRFC7638Thumbprint will generate the JWK thumbprint (https://www.rfc-editor.org/rfc/rfc7638.html) for a crypto.PublicKey.
+//
+// Copied from https://github.com/distribution/distribution/blob/51bdcb7bac069f263ce238db6bd0610759c2635f/registry/auth/token/util.go#L63
+func getRFC7638Thumbprint(publickey crypto.PublicKey) string {
+ var payload string
+
+ switch pubkey := publickey.(type) {
+ case *rsa.PublicKey:
+ e_big := big.NewInt(int64(pubkey.E)).Bytes()
+
+ e := base64.RawURLEncoding.EncodeToString(e_big)
+ n := base64.RawURLEncoding.EncodeToString(pubkey.N.Bytes())
+
+ payload = fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, e, n)
+ case *ecdsa.PublicKey:
+ params := pubkey.Params()
+ crv := params.Name
+ x := base64.RawURLEncoding.EncodeToString(params.Gx.Bytes())
+ y := base64.RawURLEncoding.EncodeToString(params.Gy.Bytes())
+
+ payload = fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, crv, x, y)
+ default:
+ return ""
+ }
+
+ shasum := sha256.Sum256([]byte(payload))
+ return base64.RawURLEncoding.EncodeToString(shasum[:])
+}
diff --git a/auth_server/server/server.go b/auth_server/server/server.go
index 54a6fa3b..ae7abd82 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -21,43 +21,73 @@ import (
"encoding/json"
"fmt"
"math/rand"
+ "net"
"net/http"
+ "regexp"
"sort"
"strings"
"time"
+ "github.com/casbin/casbin/v2"
+ "github.com/cesanta/glog"
+ "github.com/docker/distribution/registry/auth/token"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
"github.com/cesanta/docker_auth/auth_server/authn"
"github.com/cesanta/docker_auth/auth_server/authz"
- "github.com/docker/distribution/registry/auth/token"
- "github.com/golang/glog"
)
-type AuthRequest struct {
- RemoteAddr string
- User string
- Password authn.PasswordString
- ai authz.AuthRequestInfo
-}
-
-func (ar AuthRequest) String() string {
- return fmt.Sprintf("{%s:%s@%s %s}", ar.User, ar.Password, ar.RemoteAddr, ar.ai)
-}
+var (
+ hostPortRegex = regexp.MustCompile(`^(?:\[(.+)\]:\d+|([^:]+):\d+)$`)
+ scopeRegex = regexp.MustCompile(`([a-z0-9]+)(\([a-z0-9]+\))?`)
+)
type AuthServer struct {
config *Config
- authenticators []authn.Authenticator
- authorizers []authz.Authorizer
+ authenticators []api.Authenticator
+ authorizers []api.Authorizer
ga *authn.GoogleAuth
+ gha *authn.GitHubAuth
+ oidc *authn.OIDCAuth
+ glab *authn.GitlabAuth
}
func NewAuthServer(c *Config) (*AuthServer, error) {
as := &AuthServer{
config: c,
- authorizers: []authz.Authorizer{authz.NewACLAuthorizer(c.ACL)},
+ authorizers: []api.Authorizer{},
+ }
+ if c.ACL != nil {
+ staticAuthorizer, err := authz.NewACLAuthorizer(c.ACL)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, staticAuthorizer)
+ }
+ if c.ACLMongo != nil {
+ mongoAuthorizer, err := authz.NewACLMongoAuthorizer(c.ACLMongo)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, mongoAuthorizer)
+ }
+ if c.ACLXorm != nil {
+ xormAuthorizer, err := authz.NewACLXormAuthz(c.ACLXorm)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, xormAuthorizer)
+ }
+ if c.ExtAuthz != nil {
+ extAuthorizer := authz.NewExtAuthzAuthorizer(c.ExtAuthz)
+ as.authorizers = append(as.authorizers, extAuthorizer)
}
if c.Users != nil {
as.authenticators = append(as.authenticators, authn.NewStaticUserAuth(c.Users))
}
+ if c.ExtAuth != nil {
+ as.authenticators = append(as.authenticators, authn.NewExtAuth(c.ExtAuth))
+ }
if c.GoogleAuth != nil {
ga, err := authn.NewGoogleAuth(c.GoogleAuth)
if err != nil {
@@ -66,6 +96,30 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
as.authenticators = append(as.authenticators, ga)
as.ga = ga
}
+ if c.GitHubAuth != nil {
+ gha, err := authn.NewGitHubAuth(c.GitHubAuth)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, gha)
+ as.gha = gha
+ }
+ if c.OIDCAuth != nil {
+ oidc, err := authn.NewOIDCAuth(c.OIDCAuth)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, oidc)
+ as.oidc = oidc
+ }
+ if c.GitlabAuth != nil {
+ glab, err := authn.NewGitlabAuth(c.GitlabAuth)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, glab)
+ as.glab = glab
+ }
if c.LDAPAuth != nil {
la, err := authn.NewLDAPAuth(c.LDAPAuth)
if err != nil {
@@ -73,89 +127,261 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
as.authenticators = append(as.authenticators, la)
}
+ if c.MongoAuth != nil {
+ ma, err := authn.NewMongoAuth(c.MongoAuth)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, ma)
+ }
+ if c.XormAuthn != nil {
+ xa, err := authn.NewXormAuth(c.XormAuthn)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, xa)
+ }
+ if c.PluginAuthn != nil {
+ pluginAuthn, err := authn.NewPluginAuthn(c.PluginAuthn)
+ if err != nil {
+ return nil, err
+ }
+ as.authenticators = append(as.authenticators, pluginAuthn)
+ }
+ if c.PluginAuthz != nil {
+ pluginAuthz, err := authz.NewPluginAuthzAuthorizer(c.PluginAuthz)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, pluginAuthz)
+ }
+ if c.CasbinAuthz != nil {
+ enforcer, err := casbin.NewEnforcer(c.CasbinAuthz.ModelFilePath, c.CasbinAuthz.PolicyFilePath)
+ if err != nil {
+ return nil, err
+ }
+ casbinAuthz, err := authz.NewCasbinAuthorizer(enforcer)
+ if err != nil {
+ return nil, err
+ }
+ as.authorizers = append(as.authorizers, casbinAuthz)
+ }
return as, nil
}
-func (as *AuthServer) ParseRequest(req *http.Request) (*AuthRequest, error) {
- ar := &AuthRequest{RemoteAddr: req.RemoteAddr}
+type authRequest struct {
+ RemoteConnAddr string
+ RemoteAddr string
+ RemoteIP net.IP
+ User string
+ Password api.PasswordString
+ Account string
+ Service string
+ Scopes []authScope
+ Labels api.Labels
+}
+
+type authScope struct {
+ Type string
+ Class string
+ Name string
+ Actions []string
+}
+
+type authzResult struct {
+ scope authScope
+ autorizedActions []string
+}
+
+func (ar authRequest) String() string {
+ return fmt.Sprintf("{%s:%s@%s %s}", ar.User, ar.Password, ar.RemoteAddr, ar.Scopes)
+}
+
+func parseRemoteAddr(ra string) net.IP {
+ hp := hostPortRegex.FindStringSubmatch(ra)
+ if hp != nil {
+ if hp[1] != "" {
+ ra = hp[1]
+ } else if hp[2] != "" {
+ ra = hp[2]
+ }
+ }
+ res := net.ParseIP(ra)
+ return res
+}
+
+func parseScope(scope string) (string, string, error) {
+ parts := scopeRegex.FindStringSubmatch(scope)
+ if parts == nil {
+ return "", "", fmt.Errorf("malformed scope request")
+ }
+
+ switch len(parts) {
+ case 3:
+ return parts[1], "", nil
+ case 4:
+ return parts[1], parts[3], nil
+ default:
+ return "", "", fmt.Errorf("malformed scope request")
+ }
+}
+
+func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
+ ar := &authRequest{RemoteConnAddr: req.RemoteAddr, RemoteAddr: req.RemoteAddr}
+ if as.config.Server.RealIPHeader != "" {
+ hv := req.Header.Get(as.config.Server.RealIPHeader)
+ ips := strings.Split(hv, ",")
+
+ realIPPos := as.config.Server.RealIPPos
+ if realIPPos < 0 {
+ realIPPos = len(ips) + realIPPos
+ if realIPPos < 0 {
+ realIPPos = 0
+ }
+ }
+
+ ar.RemoteAddr = strings.TrimSpace(ips[realIPPos])
+ glog.V(3).Infof("Conn ip %s, %s: %s, addr: %s", ar.RemoteAddr, as.config.Server.RealIPHeader, hv, ar.RemoteAddr)
+ if ar.RemoteAddr == "" {
+ return nil, fmt.Errorf("client address not provided")
+ }
+ }
+ ar.RemoteIP = parseRemoteAddr(ar.RemoteAddr)
+ if ar.RemoteIP == nil {
+ return nil, fmt.Errorf("unable to parse remote addr %s", ar.RemoteAddr)
+ }
user, password, haveBasicAuth := req.BasicAuth()
if haveBasicAuth {
ar.User = user
- ar.Password = authn.PasswordString(password)
- }
- ar.ai.Account = req.FormValue("account")
- if ar.ai.Account == "" {
- ar.ai.Account = ar.User
- } else if haveBasicAuth && ar.ai.Account != ar.User {
- return nil, fmt.Errorf("user and account are not the same (%q vs %q)", ar.User, ar.ai.Account)
- }
- ar.ai.Service = req.FormValue("service")
- scope := req.FormValue("scope")
- if scope != "" {
- parts := strings.Split(scope, ":")
- if len(parts) != 3 {
- return nil, fmt.Errorf("invalid scope: %q", scope)
+ ar.Password = api.PasswordString(password)
+ } else if req.Method == "POST" {
+ // username and password could be part of form data
+ username := req.FormValue("username")
+ password := req.FormValue("password")
+ if username != "" && password != "" {
+ ar.User = username
+ ar.Password = api.PasswordString(password)
+ }
+ }
+ ar.Account = req.FormValue("account")
+ if ar.Account == "" {
+ ar.Account = ar.User
+ } else if haveBasicAuth && ar.Account != ar.User {
+ return nil, fmt.Errorf("user and account are not the same (%q vs %q)", ar.User, ar.Account)
+ }
+ ar.Service = req.FormValue("service")
+ if err := req.ParseForm(); err != nil {
+ return nil, fmt.Errorf("invalid form value")
+ }
+ // https://github.com/docker/distribution/blob/1b9ab303a477ded9bdd3fc97e9119fa8f9e58fca/docs/spec/auth/scope.md#resource-scope-grammar
+ if req.FormValue("scope") != "" {
+ for _, scopeValue := range req.Form["scope"] {
+ for _, scopeStr := range strings.Split(scopeValue, " ") {
+ parts := strings.Split(scopeStr, ":")
+ var scope authScope
+
+ scopeType, scopeClass, err := parseScope(parts[0])
+ if err != nil {
+ return nil, err
+ }
+
+ switch len(parts) {
+ case 3:
+ scope = authScope{
+ Type: scopeType,
+ Class: scopeClass,
+ Name: parts[1],
+ Actions: strings.Split(parts[2], ","),
+ }
+ case 4:
+ scope = authScope{
+ Type: scopeType,
+ Class: scopeClass,
+ Name: parts[1] + ":" + parts[2],
+ Actions: strings.Split(parts[3], ","),
+ }
+ default:
+ return nil, fmt.Errorf("invalid scope: %q", scopeStr)
+ }
+ sort.Strings(scope.Actions)
+ ar.Scopes = append(ar.Scopes, scope)
+ }
}
- ar.ai.Type = parts[0]
- ar.ai.Name = parts[1]
- ar.ai.Actions = strings.Split(parts[2], ",")
- sort.Strings(ar.ai.Actions)
}
return ar, nil
}
-func (as *AuthServer) Authenticate(ar *AuthRequest) (bool, error) {
+func (as *AuthServer) Authenticate(ar *authRequest) (bool, api.Labels, error) {
for i, a := range as.authenticators {
- result, err := a.Authenticate(ar.ai.Account, ar.Password)
- glog.V(2).Infof("Authn %s %s -> %t, %s", a.Name(), ar.ai.Account, result, err)
+ result, labels, err := a.Authenticate(ar.Account, ar.Password)
+ glog.V(2).Infof("Authn %s %s -> %t, %+v, %v", a.Name(), ar.Account, result, labels, err)
if err != nil {
- if err == authn.NoMatch {
+ if err == api.NoMatch {
continue
+ } else if err == api.WrongPass {
+ glog.Warningf("Failed authentication with %s: %s", err, ar.Account)
+ return false, nil, nil
}
err = fmt.Errorf("authn #%d returned error: %s", i+1, err)
glog.Errorf("%s: %s", ar, err)
- return false, err
+ return false, nil, err
}
- return result, nil
+ return result, labels, nil
}
// Deny by default.
- glog.Warningf("%s did not match any authn rule", ar.ai)
- return false, nil
+ glog.Warningf("%s did not match any authn rule", ar)
+ return false, nil, nil
}
-func (as *AuthServer) Authorize(ar *AuthRequest) ([]string, error) {
+func (as *AuthServer) authorizeScope(ai *api.AuthRequestInfo) ([]string, error) {
for i, a := range as.authorizers {
- result, err := a.Authorize(&ar.ai)
- glog.V(2).Infof("Authz %s %s -> %s, %s", a.Name(), ar.ai, result, err)
+ result, err := a.Authorize(ai)
+ glog.V(2).Infof("Authz %s %s -> %s, %s", a.Name(), *ai, result, err)
if err != nil {
- if err == authz.NoMatch {
+ if err == api.NoMatch {
continue
}
err = fmt.Errorf("authz #%d returned error: %s", i+1, err)
- glog.Errorf("%s: %s", ar, err)
- return nil, authz.NoMatch
+ glog.Errorf("%s: %s", *ai, err)
+ return nil, err
}
return result, nil
}
// Deny by default.
- glog.Warningf("%s did not match any authz rule", ar.ai)
+ glog.Warningf("%s did not match any authz rule", *ai)
return nil, nil
}
+func (as *AuthServer) Authorize(ar *authRequest) ([]authzResult, error) {
+ ares := []authzResult{}
+ for _, scope := range ar.Scopes {
+ ai := &api.AuthRequestInfo{
+ Account: ar.Account,
+ Type: scope.Type,
+ Name: scope.Name,
+ Service: ar.Service,
+ IP: ar.RemoteIP,
+ Actions: scope.Actions,
+ Labels: ar.Labels,
+ }
+ actions, err := as.authorizeScope(ai)
+ if err != nil {
+ return nil, err
+ }
+ ares = append(ares, authzResult{scope: scope, autorizedActions: actions})
+ }
+ return ares, nil
+}
+
// https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md#example
-func (as *AuthServer) CreateToken(ar *AuthRequest, actions []string) (string, error) {
+func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string, error) {
now := time.Now().Unix()
tc := &as.config.Token
- // Sign something dummy to find out which algorithm is used.
- _, sigAlg, err := tc.privateKey.Sign(strings.NewReader("dummy"), 0)
- if err != nil {
- return "", fmt.Errorf("failed to sign: %s", err)
- }
header := token.Header{
Type: "JWT",
- SigningAlg: sigAlg,
- KeyID: tc.publicKey.KeyID(),
+ SigningAlg: tc.sigAlg,
+ KeyID: tc.keyID,
}
headerJSON, err := json.Marshal(header)
if err != nil {
@@ -164,18 +390,25 @@ func (as *AuthServer) CreateToken(ar *AuthRequest, actions []string) (string, er
claims := token.ClaimSet{
Issuer: tc.Issuer,
- Subject: ar.ai.Account,
- Audience: ar.ai.Service,
- NotBefore: now - 1,
+ Subject: ar.Account,
+ Audience: ar.Service,
+ NotBefore: now - 10,
IssuedAt: now,
Expiration: now + tc.Expiration,
JWTID: fmt.Sprintf("%d", rand.Int63()),
Access: []*token.ResourceActions{},
}
- if len(actions) > 0 {
- claims.Access = []*token.ResourceActions{
- &token.ResourceActions{Type: ar.ai.Type, Name: ar.ai.Name, Actions: actions},
+ for _, a := range ares {
+ ra := &token.ResourceActions{
+ Type: a.scope.Type,
+ Name: a.scope.Name,
+ Actions: a.autorizedActions,
+ }
+ if ra.Actions == nil {
+ ra.Actions = []string{}
}
+ sort.Strings(ra.Actions)
+ claims.Access = append(claims.Access, ra)
}
claimsJSON, err := json.Marshal(claims)
if err != nil {
@@ -185,22 +418,34 @@ func (as *AuthServer) CreateToken(ar *AuthRequest, actions []string) (string, er
payload := fmt.Sprintf("%s%s%s", joseBase64UrlEncode(headerJSON), token.TokenSeparator, joseBase64UrlEncode(claimsJSON))
sig, sigAlg2, err := tc.privateKey.Sign(strings.NewReader(payload), 0)
- if err != nil || sigAlg2 != sigAlg {
+ if err != nil || sigAlg2 != tc.sigAlg {
return "", fmt.Errorf("failed to sign token: %s", err)
}
- glog.Infof("New token for %s: %s", *ar, claimsJSON)
+ glog.Infof("New token for %s %+v: %s", *ar, ar.Labels, claimsJSON)
return fmt.Sprintf("%s%s%s", payload, token.TokenSeparator, joseBase64UrlEncode(sig)), nil
}
func (as *AuthServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
glog.V(3).Infof("Request: %+v", req)
+ path_prefix := as.config.Server.PathPrefix
+ if as.config.Server.HSTS {
+ rw.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
+ }
switch {
- case req.URL.Path == "/":
+ case req.URL.Path == path_prefix+"/":
as.doIndex(rw, req)
- case req.URL.Path == "/auth":
+ case req.URL.Path == path_prefix+"/auth":
as.doAuth(rw, req)
- case req.URL.Path == "/google_auth" && as.ga != nil:
+ case req.URL.Path == path_prefix+"/auth/token":
+ as.doAuth(rw, req)
+ case req.URL.Path == path_prefix+"/google_auth" && as.ga != nil:
as.ga.DoGoogleAuth(rw, req)
+ case req.URL.Path == path_prefix+"/github_auth" && as.gha != nil:
+ as.gha.DoGitHubAuth(rw, req)
+ case req.URL.Path == path_prefix+"/oidc_auth" && as.oidc != nil:
+ as.oidc.DoOIDCAuth(rw, req)
+ case req.URL.Path == path_prefix+"/gitlab_auth" && as.glab != nil:
+ as.glab.DoGitlabAuth(rw, req)
default:
http.Error(rw, "Not found", http.StatusNotFound)
return
@@ -209,16 +454,29 @@ func (as *AuthServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// https://developers.google.com/identity/sign-in/web/server-side-flow
func (as *AuthServer) doIndex(rw http.ResponseWriter, req *http.Request) {
- rw.Header().Set("Content-Type", "text-html; charset=utf-8")
- fmt.Fprintf(rw, "