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 4fe0513a..5e00a657 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,39 @@ 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)) + * 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 - * External program + * 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`. @@ -35,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:stable /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:stable --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 ea8e804c..e63dae2a 100644 --- a/auth_server/.gitignore +++ b/auth_server/.gitignore @@ -1,4 +1,4 @@ ca-certificates.crt auth_server -Godeps/ +vendor/*/ version.* diff --git a/auth_server/Dockerfile b/auth_server/Dockerfile index 898026a3..c489ad6e 100644 --- a/auth_server/Dockerfile +++ b/auth_server/Dockerfile @@ -1,6 +1,20 @@ -FROM busybox -ADD auth_server /docker_auth/ -COPY ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +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 diff --git a/auth_server/Makefile b/auth_server/Makefile index f716b448..120d1a89 100644 --- a/auth_server/Makefile +++ b/auth_server/Makefile @@ -1,50 +1,34 @@ MAKEFLAGS += --warn-undefined-variables IMAGE ?= cesanta/docker_auth -COMPRESS_BINARY ?= false -CA_BUNDLE = /etc/ssl/certs/ca-certificates.crt -VERSION = $(shell cat version.txt) - -BUILDER_IMAGE ?= centurylink/golang-builder -BUILDER_IMAGE_EXTRA-build-cross = -cross -BUILDER_OPTS-docker-build = -v /var/run/docker.sock:/var/run/docker.sock -BUILDER_IMAGE_EXTRA-docker-build = +VERSION ?= $(shell go run ./gen_version.go | awk '{print $$1}') +BUILD_ID ?= $(shell go run ./gen_version.go | awk '{print $$2}') .PHONY: % all: build -local: build-local - -update-deps: - go get -v -u -f github.com/tools/godep github.com/jteeuwen/go-bindata/... - go generate ./... - -godep: - godep save -build-local: update-deps - go build +build: + go build -v -ldflags="-extldflags '-static' -X 'main.Version=${VERSION}' -X 'main.BuildID=${BUILD_ID}'" -ca-certificates.crt: - cp $(CA_BUNDLE) . +auth_server: + @echo + @echo Use build or build-release to produce the auth_server binary + @echo + @exit 1 docker-build: - go generate ./... - docker run --rm -v $(PWD):/src -e COMPRESS_BINARY=$(COMPRESS_BINARY) $(BUILDER_OPTS-$@) $(BUILDER_IMAGE)$(BUILDER_IMAGE_EXTRA-$@) $(IMAGE) - @echo === Built version $(VERSION) === - -build build-cross: update-deps godep ca-certificates.crt docker-build - -docker-tag: + 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-push: + docker push $(IMAGE):latest docker push $(IMAGE):$(VERSION) docker-push-%: docker-tag-% docker push $(IMAGE):$* -# Shortcut for latest -docker-push: docker-push-latest +clean: + rm -rf auth_server vendor/*/* diff --git a/auth_server/README.md b/auth_server/README.md index 34b203fb..00f30fe5 100644 --- a/auth_server/README.md +++ b/auth_server/README.md @@ -1,12 +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 -# copy ca certificate to /etc/ssl/certs/ca-certificates.crt -pip install gitpython -mkdir /var/tmp/go -export GOPATH=/var/tmp/go -export PATH=$PATH:$GOPATH/bin -make +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 69% rename from auth_server/authz/authz.go rename to auth_server/api/authz.go index 53eba0e0..6d03ead8 100644 --- a/auth_server/authz/authz.go +++ b/auth_server/api/authz.go @@ -1,12 +1,25 @@ -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" - - "github.com/cesanta/docker_auth/auth_server/authn" ) // Authorizer interface performs authorization of the request. @@ -32,8 +45,6 @@ type Authorizer interface { Name() string } -var NoMatch = errors.New("did not match any rule") - type AuthRequestInfo struct { Account string Type string @@ -41,7 +52,7 @@ type AuthRequestInfo struct { Service string IP net.IP Actions []string - Labels authn.Labels + Labels Labels } func (ai AuthRequestInfo) String() string { diff --git a/auth_server/authn/authn.go b/auth_server/authn/authn.go index 70e56e82..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,39 +16,7 @@ package authn -import "errors" +import "embed" -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") - -//go:generate go-bindata -pkg authn -modtime 1 -mode 420 -nocompress 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 08b0104b..00000000 --- a/auth_server/authn/bindata.go +++ /dev/null @@ -1,326 +0,0 @@ -// Code generated by go-bindata. -// sources: -// data/github_auth.tmpl -// data/google_auth.tmpl -// DO NOT EDIT! - -package authn - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) -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 _dataGithub_authTmpl = []byte(` -
- - - - -`) - -func dataGithub_authTmplBytes() ([]byte, error) { - return _dataGithub_authTmpl, nil -} - -func dataGithub_authTmpl() (*asset, error) { - bytes, err := dataGithub_authTmplBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "data/github_auth.tmpl", size: 350, mode: os.FileMode(420), modTime: time.Unix(1, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var _dataGoogle_authTmpl = []byte(` - - - - - - - - - - - - - -`) - -func dataGoogle_authTmplBytes() ([]byte, error) { - return _dataGoogle_authTmpl, nil -} - -func dataGoogle_authTmpl() (*asset, error) { - bytes, err := dataGoogle_authTmplBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "data/google_auth.tmpl", size: 2817, 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/github_auth.tmpl": dataGithub_authTmpl, - "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{ - "github_auth.tmpl": &bintree{dataGithub_authTmpl, 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 index 9fb86871..4ec1afc6 100644 --- a/auth_server/authn/data/github_auth.tmpl +++ b/auth_server/authn/data/github_auth.tmpl @@ -1,6 +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/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
index 9a1709ef..7c6757cc 100644
--- a/auth_server/authn/ext_auth.go
+++ b/auth_server/authn/ext_auth.go
@@ -23,7 +23,9 @@ import (
"strings"
"syscall"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
type ExtAuthConfig struct {
@@ -41,7 +43,7 @@ const (
)
type ExtAuthResponse struct {
- Labels Labels `json:"labels,omitempty"`
+ Labels api.Labels `json:"labels,omitempty"`
}
func (c *ExtAuthConfig) Validate() error {
@@ -63,7 +65,7 @@ func NewExtAuth(cfg *ExtAuthConfig) *extAuth {
return &extAuth{cfg: cfg}
}
-func (ea *extAuth) Authenticate(user string, password PasswordString) (bool, Labels, error) {
+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()
@@ -90,7 +92,7 @@ func (ea *extAuth) Authenticate(user string, password PasswordString) (bool, Lab
case ExtAuthDenied:
return false, nil, nil
case ExtAuthNoMatch:
- return false, nil, NoMatch
+ return false, nil, api.NoMatch
default:
glog.Errorf("Ext command error: %d %s", es, et)
}
diff --git a/auth_server/authn/github_auth.go b/auth_server/authn/github_auth.go
index 9bef12b2..83b8c972 100644
--- a/auth_server/authn/github_auth.go
+++ b/auth_server/authn/github_auth.go
@@ -22,23 +22,52 @@ import (
"errors"
"fmt"
"html/template"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"strings"
"time"
- "github.com/golang/glog"
+ "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"`
- TokenDB string `yaml:"token_db,omitempty"`
- HTTPTimeout time.Duration `yaml:"http_timeout,omitempty"`
- RevalidateAfter time.Duration `yaml:"revalidate_after,omitempty"`
+ 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 {
@@ -53,28 +82,127 @@ type GitHubTokenUser struct {
}
type GitHubAuth struct {
- config *GitHubAuthConfig
- db TokenDB
- client *http.Client
- tmpl *template.Template
+ 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) {
- db, err := NewTokenDB(c.TokenDB)
+ 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", c.TokenDB)
+ 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: 10 * time.Second},
- tmpl: template.Must(template.New("github_auth").Parse(string(MustAsset("data/github_auth.tmpl")))),
+ 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 string }{ClientId: gha.config.ClientId}); err != nil {
+ 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)
}
}
@@ -90,13 +218,30 @@ func (gha *GitHubAuth) DoGitHubAuth(rw http.ResponseWriter, req *http.Request) {
}
}
+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", "https://github.com/login/oauth/access_token", bytes.NewBufferString(data.Encode()))
+
+ 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
@@ -108,7 +253,7 @@ func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code stri
http.Error(rw, fmt.Sprintf("Error talking to GitHub 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))
@@ -134,10 +279,16 @@ func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code stri
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 {
@@ -146,11 +297,12 @@ func (gha *GitHubAuth) doGitHubAuthCreateToken(rw http.ResponseWriter, code stri
return
}
- fmt.Fprintf(rw, `Server logged in; now run "docker login", use %s as login and %s as password.`, user, dp)
+ gha.doGitHubAuthResultPage(rw, user, dp)
}
func (gha *GitHubAuth) validateAccessToken(token string) (user string, err error) {
- req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
+ 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
@@ -163,7 +315,7 @@ func (gha *GitHubAuth) validateAccessToken(token string) (user string, err error
err = fmt.Errorf("could not verify token %s: %s", token, err)
return
}
- body, _ := ioutil.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var ti GitHubTokenUser
@@ -187,7 +339,8 @@ func (gha *GitHubAuth) checkOrganization(token, user string) (err error) {
if gha.config.Organization == "" {
return nil
}
- url := fmt.Sprintf("https://api.github.com/orgs/%s/members/%s", gha.config.Organization, user)
+ 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)
@@ -204,7 +357,7 @@ func (gha *GitHubAuth) checkOrganization(token, user string) (err error) {
case http.StatusNoContent:
return nil
case http.StatusNotFound:
- return fmt.Errorf("%s is not a member of organization %s", user, gha.config.Organization)
+ 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)
}
@@ -212,14 +365,83 @@ func (gha *GitHubAuth) checkOrganization(token, user string) (err error) {
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.")
+ 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)
@@ -229,13 +451,25 @@ func (gha *GitHubAuth) validateServerToken(user string) (*TokenDBValue, error) {
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)
- texp := v.ValidUntil.Sub(time.Now())
- glog.V(1).Infof("Validated GitHub auth token for %s (exp %d)", user, int(texp.Seconds()))
+ 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 PasswordString) (bool, Labels, error) {
+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)
@@ -245,7 +479,16 @@ func (gha *GitHubAuth) Authenticate(user string, password PasswordString) (bool,
} else if err != nil {
return false, nil, err
}
- return true, nil, nil
+
+ 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() {
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 cd0704cb..622a7b0a 100644
--- a/auth_server/authn/google_auth.go
+++ b/auth_server/authn/google_auth.go
@@ -21,22 +21,26 @@ import (
"errors"
"fmt"
"html/template"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"strings"
"time"
- "github.com/golang/glog"
+ "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 {
@@ -125,16 +129,31 @@ type GoogleAuth struct {
}
func NewGoogleAuth(c *GoogleAuthConfig) (*GoogleAuth, error) {
- db, err := NewTokenDB(c.TokenDB)
+ 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
}
@@ -143,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)
@@ -184,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))
@@ -234,7 +253,7 @@ func (ga *GoogleAuth) doGoogleAuthCreateToken(rw http.ResponseWriter, code strin
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) {
@@ -243,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
@@ -298,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)
@@ -315,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)
@@ -399,7 +418,7 @@ func (ga *GoogleAuth) doGoogleAuthSignOut(rw http.ResponseWriter, token string)
fmt.Fprint(rw, "signed out")
}
-func (ga *GoogleAuth) Authenticate(user string, password PasswordString) (bool, Labels, error) {
+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)
diff --git a/auth_server/authn/ldap_auth.go b/auth_server/authn/ldap_auth.go
index a885b40c..cc837cd9 100644
--- a/auth_server/authn/ldap_auth.go
+++ b/auth_server/authn/ldap_auth.go
@@ -17,26 +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"`
- TLS string `yaml:"tls,omitempty"`
- InsecureTLSSkipVerify bool `yaml:"insecure_tls_skip_verify,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 {
@@ -53,9 +62,9 @@ func NewLDAPAuth(c *LDAPAuthConfig) (*LDAPAuth, error) {
}
//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, Labels, error) {
- if account == "" {
- return false, nil, 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 {
@@ -63,21 +72,36 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
}
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, nil, 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, nil, uSearchErr
}
if accountEntryDN == "" {
- return false, nil, NoMatch // User does not exist
+ 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))
@@ -89,11 +113,19 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
}
}
// Rebind as the read only user for any futher queries
- if bindErr := la.bindReadOnlyUser(l); bindErr != nil {
- return false, nil, 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, nil
+ return true, labels, nil
}
func (la *LDAPAuth) bindReadOnlyUser(l *ldap.Conn) error {
@@ -112,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
@@ -136,18 +184,38 @@ func (la *LDAPAuth) escapeAccountInput(account string) string {
func (la *LDAPAuth) ldapConnection() (*ldap.Conn, error) {
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.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(&tls.Config{InsecureSkipVerify: la.config.InsecureTLSSkipVerify}); tlserr != nil {
+ 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), &tls.Config{InsecureSkipVerify: la.config.InsecureTLSSkipVerify})
+ l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s", la.config.Addr), tlsConfig)
}
if err != nil {
return nil, err
@@ -163,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(
@@ -176,30 +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) == 0 {
- return "", nil // User does not exist
+ return "", nil, nil // User does not exist
} else if len(sr.Entries) > 1 {
- return "", fmt.Errorf("Too many entries returned.")
+ 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
index 165fee40..db546be4 100644
--- a/auth_server/authn/mongo_auth.go
+++ b/auth_server/authn/mongo_auth.go
@@ -17,16 +17,20 @@
package authn
import (
+ "context"
"errors"
"fmt"
"io"
"time"
- "github.com/cesanta/docker_auth/auth_server/mgo_session"
- "github.com/golang/glog"
+ "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"
- "gopkg.in/mgo.v2"
- "gopkg.in/mgo.v2/bson"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
+ "github.com/cesanta/docker_auth/auth_server/mgo_session"
)
type MongoAuthConfig struct {
@@ -36,13 +40,14 @@ type MongoAuthConfig struct {
type MongoAuth struct {
config *MongoAuthConfig
- session *mgo.Session
+ 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"`
+ 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) {
@@ -51,29 +56,22 @@ func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
if err != nil {
return nil, err
}
-
- // Copy our session
- tmp_session := session.Copy()
- // Close up when we are done
- defer tmp_session.Close()
-
// determine collection
- collection := tmp_session.DB(c.MongoConfig.DialInfo.Database).C(c.Collection)
+ collection := session.Database(c.MongoConfig.DialInfo.Database).Collection(c.Collection)
// Create username index obj
- index := mgo.Index{
- Key: []string{"username"},
- Unique: true,
- DropDups: false, // Error on duplicate key document instead of drop.
+ index := mongo.IndexModel{
+ Keys: bson.M{"username": 1},
+ Options: options.Index().SetUnique(true),
}
- // Enforce a username index. This is fine to do frequently per the docs:
- // https://godoc.org/gopkg.in/mgo.v2#Collection.EnsureIndex:
- // Once EnsureIndex returns successfully, following requests for the same index
- // will not contact the server unless Collection.DropIndex is used to drop the same
- // index, or Session.ResetIndexCache is called.
- if err := collection.EnsureIndex(index); err != nil {
- return nil, err
+ // 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{
@@ -82,49 +80,48 @@ func NewMongoAuth(c *MongoAuthConfig) (*MongoAuth, error) {
}, nil
}
-func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (bool, Labels, error) {
+func (mauth *MongoAuth) Authenticate(account string, password api.PasswordString) (bool, api.Labels, error) {
for true {
- result, err := mauth.authenticate(account, password)
+ 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, nil, err
+ return result, labels, err
}
return false, nil, errors.New("Unable to communicate with Mongo.")
}
-func (mauth *MongoAuth) authenticate(account string, password PasswordString) (bool, error) {
- // Copy our session
- tmp_session := mauth.session.Copy()
- // Close up when we are done
- defer tmp_session.Close()
+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 := tmp_session.DB(mauth.config.MongoConfig.DialInfo.Database).C(mauth.config.Collection)
- err := collection.Find(bson.M{"username": account}).One(&dbUserRecord)
+ 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 == mgo.ErrNotFound {
- return false, NoMatch
+ if err == mongo.ErrNoDocuments {
+ return false, nil, api.NoMatch
} else if err != nil {
- return false, err
+ 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
+ return false, nil, nil
}
}
// Auth success
- return true, nil
+ return true, dbUserRecord.Labels, nil
}
// Validate ensures that any custom config options
@@ -144,10 +141,7 @@ func (c *MongoAuthConfig) Validate(configKey string) error {
}
func (ma *MongoAuth) Stop() {
- // Close connection to MongoDB database (if any)
- if ma.session != nil {
- ma.session.Close()
- }
+
}
func (ga *MongoAuth) Name() string {
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 e7868e0e..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, Labels, error) {
+func (sua *staticUsersAuth) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) {
reqs := sua.users[user]
if reqs == nil {
- return false, nil, NoMatch
+ return false, nil, api.NoMatch
}
if reqs.Password != nil {
if bcrypt.CompareHashAndPassword([]byte(*reqs.Password), []byte(password)) != nil {
return false, nil, nil
}
}
- return true, nil, 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.go b/auth_server/authn/tokendb_level.go
similarity index 82%
rename from auth_server/authn/tokendb.go
rename to auth_server/authn/tokendb_level.go
index daaec171..66d43444 100644
--- a/auth_server/authn/tokendb.go
+++ b/auth_server/authn/tokendb_level.go
@@ -22,11 +22,12 @@ import (
"fmt"
"time"
- "golang.org/x/crypto/bcrypt"
-
+ "github.com/cesanta/glog"
"github.com/dchest/uniuri"
- "github.com/golang/glog"
"github.com/syndtr/goleveldb/leveldb"
+ "golang.org/x/crypto/bcrypt"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
const (
@@ -35,6 +36,11 @@ const (
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
@@ -46,7 +52,7 @@ type TokenDB interface {
// ValidateTOken takes a username and password
// and returns an error
- ValidateToken(string, PasswordString) error
+ ValidateToken(string, api.PasswordString) error
// DeleteToken takes a username
// and deletes the corresponding token from the DB
@@ -69,12 +75,17 @@ type TokenDBValue struct {
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"`
+ DockerPassword string `json:"docker_password,omitempty"`
+ Labels api.Labels `json:"labels,omitempty"`
}
// NewTokenDB returns a new TokenDB structure
-func NewTokenDB(file string) (TokenDB, error) {
- db, err := leveldb.OpenFile(file, nil)
+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
@@ -93,7 +104,7 @@ func (db *TokenDBImpl) GetValue(user string) (*TokenDBValue, error) {
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 nil, fmt.Errorf("bad DB value due: %v", err)
}
return &dbv, nil
}
@@ -117,16 +128,16 @@ func (db *TokenDBImpl) StoreToken(user string, v *TokenDBValue, updatePassword b
return
}
-func (db *TokenDBImpl) ValidateToken(user string, password PasswordString) error {
+func (db *TokenDBImpl) ValidateToken(user string, password api.PasswordString) error {
dbv, err := db.GetValue(user)
if err != nil {
return err
}
if dbv == nil {
- return NoMatch
+ return api.NoMatch
}
if bcrypt.CompareHashAndPassword([]byte(dbv.DockerPassword), []byte(password)) != nil {
- return WrongPass
+ return api.WrongPass
}
if time.Now().After(dbv.ValidUntil) {
return ExpiredToken
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 e58e7fd2..b0aa21c7 100644
--- a/auth_server/authz/acl.go
+++ b/auth_server/authz/acl.go
@@ -1,6 +1,7 @@
package authz
import (
+ "context"
"encoding/json"
"fmt"
"net"
@@ -10,8 +11,9 @@ import (
"strconv"
"strings"
- "github.com/cesanta/docker_auth/auth_server/authn"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
type ACL []ACLEntry
@@ -27,6 +29,7 @@ type MatchConditions struct {
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"`
}
@@ -64,7 +67,7 @@ func parseIPPattern(ipp string) (*net.IPNet, error) {
}
func validateMatchConditions(mc *MatchConditions) error {
- for _, p := range []*string{mc.Account, mc.Type, mc.Name} {
+ for _, p := range []*string{mc.Account, mc.Type, mc.Name, mc.Service} {
if p == nil {
continue
}
@@ -99,7 +102,7 @@ func ValidateACL(acl ACL) error {
}
// NewACLAuthorizer Creates a new static authorizer with ACL that have been read from the config file
-func NewACLAuthorizer(acl ACL) (Authorizer, error) {
+func NewACLAuthorizer(acl ACL) (api.Authorizer, error) {
if err := ValidateACL(acl); err != nil {
return nil, err
}
@@ -107,18 +110,22 @@ func NewACLAuthorizer(acl ACL) (Authorizer, error) {
return &aclAuthorizer{acl: acl}, nil
}
-func (aa *aclAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
+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 (Comment: %s)", ai, e, e.Comment)
+ 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() {
@@ -152,6 +159,84 @@ func matchString(pp *string, s string, vars []string) bool {
return err == nil && matched
}
+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
@@ -166,7 +251,7 @@ func matchIP(ipp *string, ip net.IP) bool {
return ipnet.Contains(ip)
}
-func matchLabels(ml map[string]string, rl authn.Labels, vars []string) bool {
+func matchLabels(ml map[string]string, rl api.Labels, vars []string) bool {
for label, pattern := range ml {
labelValues := rl[label]
matched := false
@@ -194,14 +279,14 @@ func getField(i interface{}, name string) (string, bool) {
return f.String(), true
}
-func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
+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),
}
- for _, x := range []string{"Account", "Type", "Name"} {
+ for _, x := range []string{"Account", "Type", "Name", "Service"} {
field, _ := getField(mc, x)
for _, found := range captureGroupRegex.FindAllStringSubmatch(field, -1) {
key := strings.Title(found[1])
@@ -232,13 +317,22 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool {
vars = append(vars, found[0], text[index])
}
}
- return matchString(mc.Account, ai.Account, vars) &&
- matchString(mc.Type, ai.Type, vars) &&
- matchString(mc.Name, ai.Name, vars) &&
+ 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 *AuthRequestInfo) bool {
+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
index b9194662..5439d482 100644
--- a/auth_server/authz/acl_mongo.go
+++ b/auth_server/authz/acl_mongo.go
@@ -1,15 +1,21 @@
package authz
import (
+ "context"
"errors"
"fmt"
- "github.com/cesanta/docker_auth/auth_server/mgo_session"
- "github.com/golang/glog"
- "gopkg.in/mgo.v2"
- "gopkg.in/mgo.v2/bson"
"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
@@ -29,15 +35,16 @@ type aclMongoAuthorizer struct {
lastCacheUpdate time.Time
lock sync.RWMutex
config *ACLMongoConfig
- staticAuthorizer Authorizer
- session *mgo.Session
+ 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) (Authorizer, error) {
+func NewACLMongoAuthorizer(c *ACLMongoConfig) (api.Authorizer, error) {
// Attempt to create new MongoDB session.
session, err := mgo_session.New(c.MongoConfig)
if err != nil {
@@ -60,7 +67,7 @@ func NewACLMongoAuthorizer(c *ACLMongoConfig) (Authorizer, error) {
return authorizer, nil
}
-func (ma *aclMongoAuthorizer) Authorize(ai *AuthRequestInfo) ([]string, error) {
+func (ma *aclMongoAuthorizer) Authorize(ai *api.AuthRequestInfo) ([]string, error) {
ma.lock.RLock()
defer ma.lock.RUnlock()
@@ -96,9 +103,6 @@ func (ma *aclMongoAuthorizer) Stop() {
ma.updateTicker.Stop()
// Close connection to MongoDB database (if any)
- if ma.session != nil {
- ma.session.Close()
- }
}
func (ma *aclMongoAuthorizer) Name() string {
@@ -136,35 +140,45 @@ func (ma *aclMongoAuthorizer) updateACLCache() error {
// Get ACL from MongoDB
var newACL MongoACL
- // Copy our session
- tmp_session := ma.session.Copy()
-
- // Close up when we are done
- defer tmp_session.Close()
+ collection := ma.session.Database(ma.config.MongoConfig.DialInfo.Database).Collection(ma.config.Collection)
- collection := tmp_session.DB(ma.config.MongoConfig.DialInfo.Database).C(ma.config.Collection)
-
- // Create sequence index obj
- index := mgo.Index{
- Key: []string{"seq"},
- Unique: true,
- DropDups: false, // Error on duplicate key document instead of drop.
+ // Create username index obj
+ index := mongo.IndexModel{
+ Keys: bson.M{"seq": 1},
+ Options: options.Index().SetUnique(true),
}
- // Enforce a sequence index. This is fine to do frequently per the docs:
- // https://godoc.org/gopkg.in/mgo.v2#Collection.EnsureIndex:
- // Once EnsureIndex returns successfully, following requests for the same index
- // will not contact the server unless Collection.DropIndex is used to drop the same
- // index, or Session.ResetIndexCache is called.
- if err := collection.EnsureIndex(index); err != nil {
+ // 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
- if err := collection.Find(bson.M{}).Sort("seq").All(&newACL); err != nil {
+ 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.
diff --git a/auth_server/authz/acl_test.go b/auth_server/authz/acl_test.go
index 51bb484d..1b1d6745 100644
--- a/auth_server/authz/acl_test.go
+++ b/auth_server/authz/acl_test.go
@@ -3,6 +3,8 @@ package authz
import (
"net"
"testing"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
func sp(s string) *string {
@@ -25,6 +27,9 @@ func TestValidation(t *testing.T) {
{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},
@@ -34,6 +39,7 @@ func TestValidation(t *testing.T) {
{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},
@@ -43,20 +49,26 @@ func TestValidation(t *testing.T) {
for i, c := range cases {
result := validateMatchConditions(&c.mc)
if c.ok && result != nil {
- t.Errorf("%d: %q: expected to pass, got %s", i, c.mc, result)
+ t.Errorf("%d: %v: expected to pass, got %s", i, c.mc, result)
} else if !c.ok && result == nil {
- t.Errorf("%d: %q: expected to fail, but it passed", i, c.mc)
+ t.Errorf("%d: %v: expected to fail, but it passed", i, c.mc)
}
}
}
func TestMatching(t *testing.T) {
- ai1 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz"}
- ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz",
+ 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 AuthRequestInfo
+ ai api.AuthRequestInfo
matches bool
}{
{MatchConditions{}, ai1, true},
@@ -65,25 +77,28 @@ func TestMatching(t *testing.T) {
{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}")}, AuthRequestInfo{Account: "foo", Name: "foo"}, true}, // Var subst
- {MatchConditions{Name: sp("/${account}_.*/")}, AuthRequestInfo{Account: "foo", Name: "foo_x"}, true},
- {MatchConditions{Name: sp("/${account}_.*/")}, AuthRequestInfo{Account: ".*", Name: "foo_x"}, false}, // Quoting
- {MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:1}/*`)}, AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, true},
- {MatchConditions{Account: sp(`/^(.+)@test\.com$/`), Name: sp(`${account:3}/*`)}, AuthRequestInfo{Account: "john.smith@test.com", Name: "john.smith/test"}, false},
- {MatchConditions{Account: sp(`/^(.+)@(.+?).test\.com$/`), Name: sp(`${account:1}-${account:2}/*`)}, AuthRequestInfo{Account: "john.smith@it.test.com", Name: "john.smith-it/test"}, true},
+ {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")}, AuthRequestInfo{IP: nil}, false},
- {MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
- {MatchConditions{IP: sp("127.0.0.1")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 2)}, false},
- {MatchConditions{IP: sp("127.0.0.2")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, false},
- {MatchConditions{IP: sp("127.0.0.0/8")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 1)}, true},
- {MatchConditions{IP: sp("127.0.0.0/8")}, AuthRequestInfo{IP: net.IPv4(127, 0, 0, 2)}, true},
- {MatchConditions{IP: sp("2001:db8::1")}, AuthRequestInfo{IP: nil}, false},
- {MatchConditions{IP: sp("2001:db8::1")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
- {MatchConditions{IP: sp("2001:db8::1")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, false},
- {MatchConditions{IP: sp("2001:db8::2")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, false},
- {MatchConditions{IP: sp("2001:db8::/48")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::1")}, true},
- {MatchConditions{IP: sp("2001:db8::/48")}, AuthRequestInfo{IP: net.ParseIP("2001:db8::2")}, true},
+ {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},
@@ -92,6 +107,16 @@ func TestMatching(t *testing.T) {
{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 {
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
index fdf00316..0e270b22 100644
--- a/auth_server/authz/ext_authz.go
+++ b/auth_server/authz/ext_authz.go
@@ -23,7 +23,9 @@ import (
"strings"
"syscall"
- "github.com/golang/glog"
+ "github.com/cesanta/glog"
+
+ "github.com/cesanta/docker_auth/auth_server/api"
)
type ExtAuthzConfig struct {
@@ -58,7 +60,7 @@ func NewExtAuthzAuthorizer(cfg *ExtAuthzConfig) *ExtAuthz {
return &ExtAuthz{cfg: cfg}
}
-func (ea *ExtAuthz) Authorize(ai *AuthRequestInfo) ([]string, error) {
+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)
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/gen_version.py b/auth_server/gen_version.py
deleted file mode 100755
index 47f5309c..00000000
--- a/auth_server/gen_version.py
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/usr/bin/env python
-
-import datetime
-import sys
-
-# Debian/Ubuntu: apt-get install python-git
-# PIP: pip install GitPython
-import git
-
-repo = git.Repo('.', search_parent_directories=True)
-
-
-def get_tag_for_commit(repo, commit):
- for tag in repo.tags:
- if tag.commit == commit:
- return tag.name
- return None
-
-
-if repo.head.is_detached:
- branch_or_tag = get_tag_for_commit(repo, repo.head.commit)
- if branch_or_tag is None:
- branch_or_tag = '?'
-else:
- branch_or_tag = repo.active_branch
-
-dirty = repo.is_dirty()
-
-ts = datetime.datetime.utcnow()
-build_id = '%s/%s@%s%s' % (ts.strftime('%Y%m%d-%H%M%S'),
- branch_or_tag,
- str(repo.head.commit)[:8],
- '+' if dirty else '')
-
-version = None
-if not dirty:
- version = get_tag_for_commit(repo, repo.head.commit)
-if version is None:
- version = ts.strftime('%Y%m%d%H')
-
-
-if len(sys.argv) == 1 or sys.argv[1] == '-':
- f = sys.stdout
-else:
- f = open(sys.argv[1], 'w')
-
-with open('version.go', 'w') as f:
- f.write("""\
-package main
-
-const (
-\tVersion = "{version}"
-\tBuildId = "{build_id}"
-)
-""".format(version=version, build_id=build_id))
-
-with open('version.txt', 'w') as f:
- f.write(version)
-
-f.close()
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 ad70282f..9a229de0 100644
--- a/auth_server/main.go
+++ b/auth_server/main.go
@@ -14,87 +14,170 @@
limitations under the License.
*/
-//go:generate ./gen_version.py
-
-package main // import "github.com/cesanta/docker_auth/auth_server"
+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 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, hd *httpdown.HTTP) (*server.AuthServer, httpdown.Server) {
+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())
+ }
}
+
+ 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, s
+ 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()
}
@@ -135,7 +218,9 @@ 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")
}
@@ -150,9 +235,9 @@ func (rs *RestartableServer) MaybeRestart() {
return
}
glog.Infof("Config ok, restarting server")
- rs.hs.Stop()
+ 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() {
@@ -160,19 +245,18 @@ func main() {
rand.Seed(time.Now().UnixNano())
glog.CopyStandardLogTo("INFO")
- glog.Infof("docker_auth %s build %s", Version, BuildId)
+ 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
index 5d1d2ba2..3fa253a2 100644
--- a/auth_server/mgo_session/mgo_session.go
+++ b/auth_server/mgo_session/mgo_session.go
@@ -17,19 +17,83 @@
package mgo_session
import (
+ "context"
"fmt"
"io/ioutil"
+ "net"
+ "net/url"
"strings"
"time"
- "github.com/golang/glog"
- "gopkg.in/mgo.v2"
+ "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 mgo.DialInfo `yaml:",inline"`
- PasswordFile string `yaml:"password_file,omitempty"`
+ 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
@@ -48,25 +112,54 @@ func (c *Config) Validate(configKey string) error {
return nil
}
-func New(c *Config) (*mgo.Session, error) {
- // 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.
+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)
- // Read in the password (if any)
- if c.PasswordFile != "" {
- passBuf, err := ioutil.ReadFile(c.PasswordFile)
+ session, err := DialWithInfo(&c.DialInfo, c.EnableTLS)
+ retClient = session
if err != nil {
- return nil, fmt.Errorf(`Failed to read password file "%s": %s`, c.PasswordFile, err)
+ return nil, err
}
- c.DialInfo.Password = strings.TrimSpace(string(passBuf))
}
- glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.Timeout)
+ return retClient, nil
+}
- session, err := mgo.DialWithInfo(&c.DialInfo)
- if err != nil {
- return nil, err
+func DialWithInfo(info *DialInfo, enableTLS bool) (*mongo.Client, error) {
+
+ sslActivationString := "ssl=false"
+ if enableTLS {
+ sslActivationString = "ssl=true"
}
- return session, nil
+ // 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 f6e53092..13c610b7 100644
--- a/auth_server/server/config.go
+++ b/auth_server/server/config.go
@@ -17,74 +17,182 @@
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"`
- GitHubAuth *authn.GitHubAuthConfig `yaml:"github_auth,omitempty"`
- LDAPAuth *authn.LDAPAuthConfig `yaml:"ldap_auth,omitempty"`
- MongoAuth *authn.MongoAuthConfig `yaml:"mongo_auth,omitempty"`
- ExtAuth *authn.ExtAuthConfig `yaml:"ext_auth,omitempty"`
- ACL authz.ACL `yaml:"acl,omitempty"`
- ACLMongo *authz.ACLMongoConfig `yaml:"acl_mongo,omitempty"`
- ExtAuthz *authz.ExtAuthzConfig `yaml:"ext_authz,omitempty"`
+ 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"`
- RealIPHeader string `yaml:"real_ip_header,omitempty"`
- RealIPPos int `yaml:"real_ip_pos,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.ExtAuth == nil && c.GoogleAuth == nil && c.GitHubAuth == nil && c.LDAPAuth == nil && c.MongoAuth == 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 != "" {
contents, err := ioutil.ReadFile(gac.ClientSecretFile)
@@ -93,11 +201,20 @@ 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 {
@@ -108,9 +225,18 @@ func validate(c *Config) error {
}
ghac.ClientSecret = strings.TrimSpace(string(contents))
}
- if ghac.ClientId == "" || ghac.ClientSecret == "" || ghac.TokenDB == "" {
- return errors.New("github_auth.{client_id,client_secret,token_db} are required.")
+ 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)
}
@@ -119,12 +245,70 @@ func validate(c *Config) error {
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.ACLMongo == nil && c.ExtAuthz == nil {
+ 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")
}
@@ -138,15 +322,30 @@ func validate(c *Config) error {
return err
}
}
+ 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
@@ -160,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
}
@@ -181,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)
}
@@ -193,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)
}
@@ -201,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 3ddd3ecc..ae7abd82 100644
--- a/auth_server/server/server.go
+++ b/auth_server/server/server.go
@@ -23,28 +23,39 @@ import (
"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"
+)
+
+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{},
+ authorizers: []api.Authorizer{},
}
if c.ACL != nil {
staticAuthorizer, err := authz.NewACLAuthorizer(c.ACL)
@@ -60,6 +71,13 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
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)
@@ -86,6 +104,22 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
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 {
@@ -100,6 +134,38 @@ func NewAuthServer(c *Config) (*AuthServer, error) {
}
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
}
@@ -108,15 +174,16 @@ type authRequest struct {
RemoteAddr string
RemoteIP net.IP
User string
- Password authn.PasswordString
+ Password api.PasswordString
Account string
Service string
Scopes []authScope
- Labels authn.Labels
+ Labels api.Labels
}
type authScope struct {
Type string
+ Class string
Name string
Actions []string
}
@@ -131,17 +198,34 @@ func (ar authRequest) String() string {
}
func parseRemoteAddr(ra string) net.IP {
- colonIndex := strings.LastIndex(ra, ":")
- if colonIndex > 0 && ra[colonIndex-1] >= 0x30 && ra[colonIndex-1] <= 0x39 {
- ra = ra[:colonIndex]
- }
- if ra[0] == '[' && ra[len(ra)-1] == ']' { // IPv6
- ra = ra[1 : len(ra)-1]
+ 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 != "" {
@@ -169,7 +253,15 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
user, password, haveBasicAuth := req.BasicAuth()
if haveBasicAuth {
ar.User = user
- ar.Password = authn.PasswordString(password)
+ 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 == "" {
@@ -183,41 +275,51 @@ func (as *AuthServer) ParseRequest(req *http.Request) (*authRequest, error) {
}
// https://github.com/docker/distribution/blob/1b9ab303a477ded9bdd3fc97e9119fa8f9e58fca/docs/spec/auth/scope.md#resource-scope-grammar
if req.FormValue("scope") != "" {
- for _, scopeStr := range req.Form["scope"] {
- parts := strings.Split(scopeStr, ":")
- var scope authScope
- switch len(parts) {
- case 3:
- scope = authScope{
- Type: parts[0],
- Name: parts[1],
- Actions: strings.Split(parts[2], ","),
+ 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
}
- case 4:
- scope = authScope{
- Type: parts[0],
- Name: parts[1] + ":" + parts[2],
- Actions: strings.Split(parts[3], ","),
+
+ 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)
}
- default:
- return nil, fmt.Errorf("invalid scope: %q", scopeStr)
+ sort.Strings(scope.Actions)
+ ar.Scopes = append(ar.Scopes, scope)
}
- sort.Strings(scope.Actions)
- ar.Scopes = append(ar.Scopes, scope)
}
}
return ar, nil
}
-func (as *AuthServer) Authenticate(ar *authRequest) (bool, authn.Labels, error) {
+func (as *AuthServer) Authenticate(ar *authRequest) (bool, api.Labels, error) {
for i, a := range as.authenticators {
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 == authn.WrongPass {
- glog.Warningf("Failed authentication with %s: %s", err)
+ } 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)
@@ -231,12 +333,12 @@ func (as *AuthServer) Authenticate(ar *authRequest) (bool, authn.Labels, error)
return false, nil, nil
}
-func (as *AuthServer) authorizeScope(ai *authz.AuthRequestInfo) ([]string, error) {
+func (as *AuthServer) authorizeScope(ai *api.AuthRequestInfo) ([]string, error) {
for i, a := range as.authorizers {
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)
@@ -253,7 +355,7 @@ func (as *AuthServer) authorizeScope(ai *authz.AuthRequestInfo) ([]string, error
func (as *AuthServer) Authorize(ar *authRequest) ([]authzResult, error) {
ares := []authzResult{}
for _, scope := range ar.Scopes {
- ai := &authz.AuthRequestInfo{
+ ai := &api.AuthRequestInfo{
Account: ar.Account,
Type: scope.Type,
Name: scope.Name,
@@ -276,15 +378,10 @@ func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string,
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 {
@@ -321,7 +418,7 @@ func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string,
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 %+v: %s", *ar, ar.Labels, claimsJSON)
@@ -330,15 +427,25 @@ func (as *AuthServer) CreateToken(ar *authRequest, ares []authzResult) (string,
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 == "/github_auth" && as.gha != nil:
+ 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
@@ -347,13 +454,23 @@ 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, "