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 dec6f38e..5e00a657 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,37 @@ 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](https://github.com/cesanta/docker_auth/blob/master/examples/ext_auth.sh) + * 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](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: - - `:latest` - bleeding edge, usually works but breaking config changes are possible. You probably do not want to use this in production. + - `: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. @@ -45,7 +55,7 @@ $ docker run \ 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 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 d1d136b9..120d1a89 100644 --- a/auth_server/Makefile +++ b/auth_server/Makefile @@ -1,42 +1,14 @@ 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 ?= golang:1.12.8-alpine +VERSION ?= $(shell go run ./gen_version.go | awk '{print $$1}') +BUILD_ID ?= $(shell go run ./gen_version.go | awk '{print $$2}') .PHONY: % all: build -deps: - go install -v github.com/a-urth/go-bindata/go-bindata - -generate: - go generate \ - github.com/cesanta/docker_auth/auth_server \ - github.com/cesanta/docker_auth/auth_server/authn/... \ - github.com/cesanta/docker_auth/auth_server/authz/... \ - github.com/cesanta/docker_auth/auth_server/mgo_session/... \ - github.com/cesanta/docker_auth/auth_server/server/... - build: - CGO_ENABLED=0 go build -v --ldflags=--s - -ca-certificates.crt: - cp $(CA_BUNDLE) . - -build-release: ca-certificates.crt - docker run --rm -v $(PWD)/..:/src \ - $(BUILDER_IMAGE) sh -x -c "\ - apk update && apk add git make py2-pip && pip install GitPython && \ - cd /src/auth_server && \ - umask 0 && \ - go install -v github.com/a-urth/go-bindata/go-bindata && \ - make generate && \ - CGO_ENABLED=0 go build -v --ldflags=--s" - @echo === Built version $$(cat version.txt) === + go build -v -ldflags="-extldflags '-static' -X 'main.Version=${VERSION}' -X 'main.BuildID=${BUILD_ID}'" auth_server: @echo @@ -45,7 +17,7 @@ auth_server: @exit 1 docker-build: - docker build -t $(IMAGE):latest . + docker build --build-arg VERSION="${VERSION}" --build-arg BUILD_ID="${BUILD_ID}" -t $(IMAGE):latest . docker tag $(IMAGE):latest $(IMAGE):$(VERSION) docker-tag-%: diff --git a/auth_server/README.md b/auth_server/README.md index 7911f999..00f30fe5 100644 --- a/auth_server/README.md +++ b/auth_server/README.md @@ -1,17 +1,9 @@ ### Building local image ``` -# copy ca certificate to /etc/ssl/certs/ca-certificates.crt -pip install gitpython 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 -export GOPATH=/var/tmp/go -export PATH=$PATH:$GOPATH/bin -# download dependencies -make deps -# build source -make generate -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 a37fc695..00000000 --- a/auth_server/authn/bindata.go +++ /dev/null @@ -1,466 +0,0 @@ -// Code generated by go-bindata. -// sources: -// data/github_auth.tmpl -// data/github_auth_result.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(` - - - - - Docker Registry Authentication - - - - -
-

- - - Login{{if .Organization}} to @{{.Organization}}{{end}} with GitHub - -

-

- Revoke access -

-
- - -`) - -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: 2946, mode: os.FileMode(420), modTime: time.Unix(1, 0)} - a := &asset{bytes: bytes, info: info} - return a, nil -} - -var _dataGithub_auth_resultTmpl = []byte(` - - - - - Docker Registry Authentication - - - -

- You are successfully authenticated for the Docker Registry{{if .Organization}} with the @{{.Organization}} Github organization{{end}}. - Use the following username and password to login into the registry: -

-
-
$ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
- - -`) - -func dataGithub_auth_resultTmplBytes() ([]byte, error) { - return _dataGithub_auth_resultTmpl, nil -} - -func dataGithub_auth_resultTmpl() (*asset, error) { - bytes, err := dataGithub_auth_resultTmplBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "data/github_auth_result.tmpl", size: 1300, 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/github_auth_result.tmpl": dataGithub_auth_resultTmpl, - "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{}}, - "github_auth_result.tmpl": &bintree{dataGithub_auth_resultTmpl, 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_result.tmpl b/auth_server/authn/data/github_auth_result.tmpl index 2619d0cd..513034ea 100644 --- a/auth_server/authn/data/github_auth_result.tmpl +++ b/auth_server/authn/data/github_auth_result.tmpl @@ -43,9 +43,11 @@

You are successfully authenticated for the Docker Registry{{if .Organization}} with the @{{.Organization}} Github organization{{end}}. - Use the following username and password to login into the registry: + Log into the registry using one of these commands:


$ 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 Registry Authentication + + + + +
+

+ +

+

+ + Login + +

+

+ Revoke access +

+
+ + diff --git a/auth_server/authn/data/gitlab_auth_result.tmpl b/auth_server/authn/data/gitlab_auth_result.tmpl new file mode 100644 index 00000000..cb53fcf8 --- /dev/null +++ b/auth_server/authn/data/gitlab_auth_result.tmpl @@ -0,0 +1,53 @@ + + + + + + Docker Registry Authentication + + + +

+ You are successfully authenticated to the Docker Registry. + Log into the registry using one of these commands: +

+
+
$ 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 Registry Authentication + + + +
+

+ + Login with OIDC Provider + +

+
+ + diff --git a/auth_server/authn/data/oidc_auth_result.tmpl b/auth_server/authn/data/oidc_auth_result.tmpl new file mode 100644 index 00000000..86021bc0 --- /dev/null +++ b/auth_server/authn/data/oidc_auth_result.tmpl @@ -0,0 +1,19 @@ + + + + + + Docker Registry Authentication + + + +

+ You are successfully authenticated for the Docker Registry. + Log into the registry using one of these commands: +

+
+
$ 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 c26b660e..7c6757cc 100644 --- a/auth_server/authn/ext_auth.go +++ b/auth_server/authn/ext_auth.go @@ -24,6 +24,8 @@ import ( "syscall" "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 1d935370..83b8c972 100644 --- a/auth_server/authn/github_auth.go +++ b/auth_server/authn/github_auth.go @@ -22,13 +22,15 @@ import ( "errors" "fmt" "html/template" - "io/ioutil" + "io" "net/http" "net/url" "strings" "time" "github.com/cesanta/glog" + + "github.com/cesanta/docker_auth/auth_server/api" ) type GitHubTeamCollection []GitHubTeam @@ -54,22 +56,18 @@ type ParentGitHubTeam struct { } 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"` - GCSTokenDB *GitHubGCSStoreConfig `yaml:"gcs_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 GitHubGCSStoreConfig struct { - Bucket string `yaml:"bucket,omitempty"` - ClientSecretFile string `yaml:"client_secret_file,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 { @@ -119,7 +117,6 @@ func execGHExperimentalApiRequest(url string, token string) (*http.Response, err } // removeSubstringsFromString removes all occurences of stringsToStrip from sourceStr -// func removeSubstringsFromString(sourceStr string, stringsToStrip []string) string { theNewString := sourceStr for _, i := range stringsToStrip { @@ -131,7 +128,6 @@ func removeSubstringsFromString(sourceStr string, stringsToStrip []string) strin // 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 < > @@ -160,24 +156,32 @@ func parseLinkHeader(linkLines []string) (linkHeader, error) { func NewGitHubAuth(c *GitHubAuthConfig) (*GitHubAuth, error) { var db TokenDB var err error - dbName := c.TokenDB - if c.GCSTokenDB == nil { - db, err = NewTokenDB(c.TokenDB) - } else { - db, err = NewGCSTokenDB(c.GCSTokenDB.Bucket, c.GCSTokenDB.ClientSecretFile) + var dbName string + + switch { + case c.GCSTokenDB != nil: + db, err = NewGCSTokenDB(c.GCSTokenDB) dbName = "GCS: " + c.GCSTokenDB.Bucket + case c.RedisTokenDB != nil: + db, err = NewRedisTokenDB(c.RedisTokenDB) + dbName = db.(*redisTokenDB).String() + default: + db, err = NewTokenDB(c.LevelTokenDB) + dbName = c.LevelTokenDB.Path } if err != nil { return nil, err } glog.Infof("GitHub auth token DB at %s", dbName) + github_auth, _ := static.ReadFile("data/github_auth.tmpl") + github_auth_result, _ := static.ReadFile("data/github_auth_result.tmpl") return &GitHubAuth{ config: c, db: db, - client: &http.Client{Timeout: 10 * time.Second}, - tmpl: template.Must(template.New("github_auth").Parse(string(MustAsset("data/github_auth.tmpl")))), - tmplResult: template.Must(template.New("github_auth_result").Parse(string(MustAsset("data/github_auth_result.tmpl")))), + 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 } @@ -249,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)) @@ -311,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 @@ -380,7 +384,7 @@ func (gha *GitHubAuth) fetchTeams(token string) ([]string, error) { } respHeaders := resp.Header - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) resp.Body.Close() err = json.Unmarshal(body, &pagedTeams) @@ -465,7 +469,7 @@ func (gha *GitHubAuth) validateServerToken(user string) (*TokenDBValue, error) { 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) 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 10891ab8..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/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)) @@ -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 3b96d004..cc837cd9 100644 --- a/auth_server/authn/ldap_auth.go +++ b/auth_server/authn/ldap_auth.go @@ -25,11 +25,14 @@ import ( "github.com/cesanta/glog" "github.com/go-ldap/ldap" + + "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 { @@ -42,6 +45,7 @@ type LDAPAuthConfig struct { 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 { @@ -58,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) { +func (la *LDAPAuth) Authenticate(account string, password api.PasswordString) (bool, api.Labels, error) { if account == "" || password == "" { - return false, nil, NoMatch + return false, nil, api.NoMatch } l, err := la.ldapConnection() if err != nil { @@ -68,12 +72,20 @@ 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) @@ -87,7 +99,7 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool, 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 @@ -101,8 +113,10 @@ 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 @@ -130,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 @@ -270,6 +300,11 @@ func (la *LDAPAuth) getLabelsFromMap(attrMap map[string][]string) (map[string][] mappingValues[i] = cn } } + if mapping.LowerCase { + for i, value := range mappingValues { + mappingValues[i] = strings.ToLower(value) + } + } labels[key] = mappingValues } } diff --git a/auth_server/authn/mongo_auth.go b/auth_server/authn/mongo_auth.go index c6aee896..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/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,14 +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"` - Labels Labels `yaml:"labels,omitempty" json:"labels,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) { @@ -52,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{ @@ -83,7 +80,7 @@ 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, labels, err := mauth.authenticate(account, password) if err == io.EOF { @@ -97,22 +94,21 @@ func (mauth *MongoAuth) Authenticate(account string, password PasswordString) (b return false, nil, errors.New("Unable to communicate with Mongo.") } -func (mauth *MongoAuth) authenticate(account string, password PasswordString) (bool, Labels, 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, nil, NoMatch + if err == mongo.ErrNoDocuments { + return false, nil, api.NoMatch } else if err != nil { return false, nil, err } @@ -145,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 index dd056e5f..cebf3230 100644 --- a/auth_server/authn/plugin_authn.go +++ b/auth_server/authn/plugin_authn.go @@ -21,13 +21,15 @@ import ( "plugin" "github.com/cesanta/glog" + + "github.com/cesanta/docker_auth/auth_server/api" ) type PluginAuthnConfig struct { PluginPath string `yaml:"plugin_path"` } -func lookupSymbol(cfg *PluginAuthnConfig) (Authenticator, error) { +func lookupAuthnSymbol(cfg *PluginAuthnConfig) (api.Authenticator, error) { // load module plug, err := plugin.Open(cfg.PluginPath) if err != nil { @@ -41,8 +43,8 @@ func lookupSymbol(cfg *PluginAuthnConfig) (Authenticator, error) { } // assert that loaded symbol is of a desired type - var authn Authenticator - authn, ok := symAuthen.(Authenticator) + 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") } @@ -50,16 +52,16 @@ func lookupSymbol(cfg *PluginAuthnConfig) (Authenticator, error) { } func (c *PluginAuthnConfig) Validate() error { - _, err := lookupSymbol(c) + _, err := lookupAuthnSymbol(c) return err } type PluginAuthn struct { cfg *PluginAuthnConfig - Authn Authenticator + Authn api.Authenticator } -func (c *PluginAuthn) Authenticate(user string, password PasswordString) (bool, Labels, error) { +func (c *PluginAuthn) Authenticate(user string, password api.PasswordString) (bool, api.Labels, error) { // use the plugin return c.Authn.Authenticate(user, password) } @@ -73,7 +75,7 @@ func (c *PluginAuthn) Name() string { func NewPluginAuthn(cfg *PluginAuthnConfig) (*PluginAuthn, error) { glog.Infof("Plugin authenticator: %s", cfg) - authn, err := lookupSymbol(cfg) + authn, err := lookupAuthnSymbol(cfg) if err != nil { return nil, err } diff --git a/auth_server/authn/static_auth.go b/auth_server/authn/static_auth.go index 7f6d1f49..4edc4892 100644 --- a/auth_server/authn/static_auth.go +++ b/auth_server/authn/static_auth.go @@ -19,11 +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"` - Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` + Password *api.PasswordString `yaml:"password,omitempty" json:"password,omitempty"` + Labels api.Labels `yaml:"labels,omitempty" json:"labels,omitempty"` } type staticUsersAuth struct { @@ -33,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) @@ -45,10 +47,10 @@ 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 { diff --git a/auth_server/authn/tokendb_gcs.go b/auth_server/authn/tokendb_gcs.go index 68158ff1..53a0d278 100644 --- a/auth_server/authn/tokendb_gcs.go +++ b/auth_server/authn/tokendb_gcs.go @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ + package authn import ( @@ -26,20 +27,33 @@ import ( "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(bucket, clientSecretFile string) (TokenDB, error) { - gcs, err := storage.NewClient(context.Background(), option.WithServiceAccountFile(clientSecretFile)) - return &gcsTokenDB{gcs, bucket}, err +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 @@ -69,7 +83,7 @@ func (db *gcsTokenDB) GetValue(user string) (*TokenDBValue, error) { func (db *gcsTokenDB) StoreToken(user string, v *TokenDBValue, updatePassword bool) (dp string, err error) { if updatePassword { dp = uniuri.New() - dph, _ := bcrypt.GenerateFromPassword([]byte(dp), bcrypt.DefaultCost) + dph, _ := bcrypt.GenerateFromPassword([]byte(dp), db.tokenHashCost) v.DockerPassword = string(dph) } @@ -86,17 +100,17 @@ func (db *gcsTokenDB) StoreToken(user string, v *TokenDBValue, updatePassword bo // 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 PasswordString) error { +func (db *gcsTokenDB) 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.go b/auth_server/authn/tokendb_level.go similarity index 84% rename from auth_server/authn/tokendb.go rename to auth_server/authn/tokendb_level.go index 96b37ec6..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/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,13 +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"` - Labels Labels `json:"labels,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 @@ -118,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 fe141fa9..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,9 +11,9 @@ import ( "strconv" "strings" - "github.com/cesanta/docker_auth/auth_server/authn" "github.com/cesanta/glog" - "github.com/schwarmco/go-cartesian-product" + + "github.com/cesanta/docker_auth/auth_server/api" ) type ACL []ACLEntry @@ -101,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 } @@ -109,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() { @@ -175,14 +180,17 @@ func matchStringWithLabelPermutations(pp *string, s string, vars []string, label } } if len(labelSets) > 0 { - for permuation := range cartesian.Iter(labelSets...) { + 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 { - break + return matched } } } @@ -190,6 +198,45 @@ func matchStringWithLabelPermutations(pp *string, s string, vars []string, label 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 @@ -204,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 @@ -232,7 +279,7 @@ 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), @@ -286,6 +333,6 @@ func (mc *MatchConditions) Matches(ai *AuthRequestInfo) bool { 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 a5441103..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/cesanta/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() + collection := ma.session.Database(ma.config.MongoConfig.DialInfo.Database).Collection(ma.config.Collection) - // Close up when we are done - defer tmp_session.Close() - - 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 3a8bd31d..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 { @@ -47,26 +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", Service: "notary"} - ai2 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "baz", Service: "notary", + 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 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "admins/foo", Service: "notary", + ai3 := api.AuthRequestInfo{Account: "foo", Type: "bar", Name: "admins/foo", Service: "notary", Labels: map[string][]string{"group": []string{"admins", "VIP"}}} - ai4 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "VIP/api", Service: "notary", + 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 := AuthRequestInfo{Account: "foo", Type: "bar", Name: "devs/api", Service: "notary", + 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}, @@ -75,28 +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}, 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 98890214..0e270b22 100644 --- a/auth_server/authz/ext_authz.go +++ b/auth_server/authz/ext_authz.go @@ -24,6 +24,8 @@ import ( "syscall" "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 index f8d1c8cd..29909bca 100644 --- a/auth_server/authz/plugin_authz.go +++ b/auth_server/authz/plugin_authz.go @@ -21,13 +21,15 @@ import ( "plugin" "github.com/cesanta/glog" + + "github.com/cesanta/docker_auth/auth_server/api" ) type PluginAuthzConfig struct { PluginPath string `yaml:"plugin_path"` } -func lookupSymbol(cfg *PluginAuthzConfig) (Authorizer, error) { +func lookupAuthzSymbol(cfg *PluginAuthzConfig) (api.Authorizer, error) { // load module plug, err := plugin.Open(cfg.PluginPath) if err != nil { @@ -41,8 +43,8 @@ func lookupSymbol(cfg *PluginAuthzConfig) (Authorizer, error) { } // assert that loaded symbol is of a desired type - var authz Authorizer - authz, ok := symAuthen.(Authorizer) + 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") } @@ -50,13 +52,12 @@ func lookupSymbol(cfg *PluginAuthzConfig) (Authorizer, error) { } func (c *PluginAuthzConfig) Validate() error { - _, err := lookupSymbol(c) + _, err := lookupAuthzSymbol(c) return err } type PluginAuthz struct { - cfg *PluginAuthzConfig - Authz Authorizer + Authz api.Authorizer } func (c *PluginAuthz) Stop() { @@ -68,14 +69,14 @@ func (c *PluginAuthz) Name() string { func NewPluginAuthzAuthorizer(cfg *PluginAuthzConfig) (*PluginAuthz, error) { glog.Infof("Plugin authorization: %s", cfg) - authz, err := lookupSymbol(cfg) + authz, err := lookupAuthzSymbol(cfg) if err != nil { return nil, err } return &PluginAuthz{Authz: authz}, nil } -func (c *PluginAuthz) Authorize(ai *AuthRequestInfo) ([]string, error) { +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 index eb97e726..2a245dc6 100644 --- a/auth_server/go.mod +++ b/auth_server/go.mod @@ -1,28 +1,83 @@ module github.com/cesanta/docker_auth/auth_server -go 1.12 +go 1.23.0 require ( - cloud.google.com/go v0.44.3 - github.com/a-urth/go-bindata v0.0.0-20180209162145-df38da164efc // indirect + 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/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 - github.com/deckarep/golang-set v1.7.1 - github.com/docker/distribution v2.7.1+incompatible + 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/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect - github.com/facebookgo/httpdown v0.0.0-20180706035922-5979d39b15c2 - github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 // indirect github.com/go-ldap/ldap v3.0.3+incompatible - github.com/gorilla/mux v1.7.3 // indirect - github.com/schwarmco/go-cartesian-product v0.0.0-20180515110546-d5ee747a6dc9 - github.com/sirupsen/logrus v1.4.2 // indirect + 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 - golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 - golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 - google.golang.org/api v0.9.0 - gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect + 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.2.2 + 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 index 6b7bca4f..e956374b 100644 --- a/auth_server/go.sum +++ b/auth_server/go.sum @@ -1,172 +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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.3 h1:0sMegbmn/8uTwpNkB0q9cLEpZ2W5a6kl+wtBQgPWBJQ= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +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/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/a-urth/go-bindata v0.0.0-20180209162145-df38da164efc h1:eXJIPWW4y4xjPWda/7ruw5PRsfcbzKTL9EhNQrBRsOU= -github.com/a-urth/go-bindata v0.0.0-20180209162145-df38da164efc/go.mod h1:D0SbCgK4DQtSNzDQzfek273VqkCnHdFCd+q2ueHGRiE= +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-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= -github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= -github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= -github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +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/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= -github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= -github.com/facebookgo/httpdown v0.0.0-20180706035922-5979d39b15c2 h1:nXeeRHmgNgjLxi+7dY9l9aDvSS1uwVlNLqUWIY4Ath0= -github.com/facebookgo/httpdown v0.0.0-20180706035922-5979d39b15c2/go.mod h1:TUV/fX3XrTtBQb5+ttSUJzcFgLNpILONFTKmBuk5RSw= -github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 h1:0YtRCqIZs2+Tz49QuH6cJVw/IFqzo39gEqZ0iYLxD2M= -github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4/go.mod h1:vsJz7uE339KUCpBXx3JAJzSRH7Uk4iGGyJzR529qDIA= +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.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +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 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +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/schwarmco/go-cartesian-product v0.0.0-20180515110546-d5ee747a6dc9 h1:rIlaPhb87A5GJy0FbjlxesD2lyr052gS/pF6NSAvSEo= -github.com/schwarmco/go-cartesian-product v0.0.0-20180515110546-d5ee747a6dc9/go.mod h1:0jtE6j9sPEDD6gfLzxwt1eF2VI6u/w1sQ99IuZcUfyk= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +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= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +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/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 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-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= 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-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20190312151545-0bb0c0a6e846/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-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +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-20190418145605-e7d98fc518a7/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-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64 h1:iKtrH9Y8mcbADOP0YFaEMth7OfuHY9xHOwNj4znpM1A= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +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.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +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 eb1f0eb8..9a229de0 100644 --- a/auth_server/main.go +++ b/auth_server/main.go @@ -14,43 +14,95 @@ 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/cesanta/glog" - "github.com/facebookgo/httpdown" "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) } - tlsConfig := &tls.Config{ - PreferServerCipherSuites: true, + 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. @@ -79,22 +131,53 @@ func ServeOnce(c *server.Config, cf string, hd *httpdown.HTTP) (*server.AuthServ 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 f06719f7..3fa253a2 100644 --- a/auth_server/mgo_session/mgo_session.go +++ b/auth_server/mgo_session/mgo_session.go @@ -17,22 +17,83 @@ package mgo_session import ( - "crypto/tls" + "context" "fmt" "io/ioutil" "net" + "net/url" "strings" "time" "github.com/cesanta/glog" - "gopkg.in/mgo.v2" + + "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"` - EnableTLS bool `yaml:"enable_tls,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 @@ -51,31 +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)) + } - // Read in the password (if any) - if c.PasswordFile != "" { - passBuf, err := ioutil.ReadFile(c.PasswordFile) + glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.Timeout) + + session, err := DialWithInfo(&c.DialInfo, c.EnableTLS) + retClient = session if err != nil { - return nil, fmt.Errorf(`Failed to read password file "%s": %s`, c.PasswordFile, err) + return nil, err } - c.DialInfo.Password = strings.TrimSpace(string(passBuf)) } - if c.EnableTLS { - c.DialInfo.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) { - return tls.Dial("tcp", addr.String(), &tls.Config{}) - } + return retClient, nil +} + +func DialWithInfo(info *DialInfo, enableTLS bool) (*mongo.Client, error) { + + sslActivationString := "ssl=false" + if enableTLS { + sslActivationString = "ssl=true" } - glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.Timeout) + // Connect + username := url.QueryEscape(info.Username) + password := url.QueryEscape(info.Password) + uri := "mongodb://" + username + ":" + password + "@" + info.Addrs[0] + "/?authSource=admin&" + sslActivationString - session, err := mgo.DialWithInfo(&c.DialInfo) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) if err != nil { - return nil, err + panic(err) + } else { + fmt.Println("Successfully connected!") } - - return session, nil + return client, err } diff --git a/auth_server/server/config.go b/auth_server/server/config.go index ffe2fcb2..13c610b7 100644 --- a/auth_server/server/config.go +++ b/auth_server/server/config.go @@ -17,19 +17,26 @@ 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 { @@ -38,27 +45,38 @@ type Config struct { 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"` - 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"` - LetsEncrypt LetsEncryptConfig `yaml:"letsencrypt,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 { @@ -68,37 +86,113 @@ type LetsEncryptConfig struct { } 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 && 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.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) @@ -107,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 { @@ -122,13 +225,18 @@ func validate(c *Config) error { } ghac.ClientSecret = strings.TrimSpace(string(contents)) } - if ghac.ClientId == "" || ghac.ClientSecret == "" || (ghac.TokenDB == "" && ghac.GCSTokenDB == nil) { - 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) } @@ -137,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 && c.PluginAuthz == 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") } @@ -156,6 +322,11 @@ 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 @@ -174,7 +345,7 @@ func validate(c *Config) error { 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 @@ -188,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 } @@ -209,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) } @@ -221,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) } @@ -229,7 +405,7 @@ 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 } @@ -237,6 +413,12 @@ func LoadConfig(fileName string) (*Config, error) { 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") @@ -251,3 +433,32 @@ func LoadConfig(fileName string) (*Config, error) { 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 9c33354c..ae7abd82 100644 --- a/auth_server/server/server.go +++ b/auth_server/server/server.go @@ -28,28 +28,34 @@ import ( "strings" "time" - "github.com/cesanta/docker_auth/auth_server/authn" - "github.com/cesanta/docker_auth/auth_server/authz" + "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" ) var ( - hostPortRegex = regexp.MustCompile(`\[?(.+?)\]?:\d+$`) + 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) @@ -65,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) @@ -91,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 { @@ -105,6 +134,13 @@ 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 { @@ -119,6 +155,17 @@ func NewAuthServer(c *Config) (*AuthServer, error) { } 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 } @@ -127,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 } @@ -152,12 +200,32 @@ func (ar authRequest) String() string { func parseRemoteAddr(ra string) net.IP { hp := hostPortRegex.FindStringSubmatch(ra) if hp != nil { - ra = string(hp[1]) + 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 != "" { @@ -185,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 == "" { @@ -199,40 +275,50 @@ 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 { + } else if err == api.WrongPass { glog.Warningf("Failed authentication with %s: %s", err, ar.Account) return false, nil, nil } @@ -247,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) @@ -269,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, @@ -292,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 { @@ -337,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) @@ -347,15 +428,24 @@ 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 == path_prefix+"/": as.doIndex(rw, req) case req.URL.Path == path_prefix+"/auth": as.doAuth(rw, req) + case req.URL.Path == path_prefix+"/auth/token": + as.doAuth(rw, req) case req.URL.Path == path_prefix+"/google_auth" && as.ga != nil: as.ga.DoGoogleAuth(rw, req) case req.URL.Path == path_prefix+"/github_auth" && as.gha != nil: as.gha.DoGitHubAuth(rw, req) + case req.URL.Path == path_prefix+"/oidc_auth" && as.oidc != nil: + as.oidc.DoOIDCAuth(rw, req) + case req.URL.Path == path_prefix+"/gitlab_auth" && as.glab != nil: + as.glab.DoGitlabAuth(rw, req) default: http.Error(rw, "Not found", http.StatusNotFound) return @@ -372,6 +462,12 @@ func (as *AuthServer) doIndex(rw http.ResponseWriter, req *http.Request) { case as.gha != nil: url := as.config.Server.PathPrefix + "/github_auth" http.Redirect(rw, req, url, 301) + case as.oidc != nil: + url := as.config.Server.PathPrefix + "/oidc_auth" + http.Redirect(rw, req, url, 301) + case as.glab != nil: + url := as.config.Server.PathPrefix + "/gitlab_auth" + http.Redirect(rw, req, url, 301) default: rw.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(rw, "

%s

\n", as.config.Token.Issuer) @@ -417,7 +513,11 @@ func (as *AuthServer) doAuth(rw http.ResponseWriter, req *http.Request) { glog.Errorf("%s: %s", ar, msg) return } - result, _ := json.Marshal(&map[string]string{"token": token}) + // https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ + // describes that the response should have the token in `access_token` + // https://docs.docker.com/registry/spec/auth/token/#token-response-fields + // the token should also be in `token` to support older clients + result, _ := json.Marshal(&map[string]string{"access_token": token, "token": token}) glog.V(3).Infof("%s", result) rw.Header().Set("Content-Type", "application/json") rw.Write(result) diff --git a/chart/docker-auth/.helmignore b/chart/docker-auth/.helmignore new file mode 100644 index 00000000..66eca035 --- /dev/null +++ b/chart/docker-auth/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +Makefile diff --git a/chart/docker-auth/Chart.yaml b/chart/docker-auth/Chart.yaml new file mode 100644 index 00000000..41af3462 --- /dev/null +++ b/chart/docker-auth/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +appVersion: "1.14.0" +description: Docker Registry V2 authentication server +name: docker-auth +version: 1.14.1 +kubeVersion: ">=1.25" +keywords: +- docker +- registry +- docker-auth +- docker-registry +- token +home: https://github.com/cesanta/docker_auth +sources: +- https://github.com/cesanta/docker_auth +maintainers: +- name: duyanghao + email: 1294057873@qq.com +- name: pfisterer + email: github@farberg.de +- name: techknowlogick + email: hello@techknowlogick.com +engine: gotpl diff --git a/chart/docker-auth/Makefile b/chart/docker-auth/Makefile new file mode 100644 index 00000000..7b34cc68 --- /dev/null +++ b/chart/docker-auth/Makefile @@ -0,0 +1,35 @@ +CHART_NAME := docker-auth +CHART_VERSION := $(shell grep '^version:' Chart.yaml | cut -d' ' -f2) +PACKAGE_NAME := $(CHART_NAME)-$(CHART_VERSION).tgz + +# Repository settings +REPO_URL := https://cesanta.github.io/docker_auth/ +DOCS_DIR := ../../docs + +.PHONY: lint +lint: + helm lint . + +.PHONY: test +test: + helm template test-release . --dry-run > /dev/null + +.PHONY: validate +validate: lint test ## Run all validation checks + @echo "All validations passed" + +.PHONY: package +package: validate ## Package the helm chart + helm package . + +.PHONY: update-repo +update-repo: package + mv $(PACKAGE_NAME) $(DOCS_DIR)/ + helm repo index $(DOCS_DIR)/ --url $(REPO_URL) + @echo "Repository updated" + @echo "" + @echo "Please review changes, then commit and push the changes to GitHub." + +.PHONY: debug +debug: + helm template debug-$(CHART_NAME) . --debug diff --git a/chart/docker-auth/README.md b/chart/docker-auth/README.md new file mode 100644 index 00000000..330946a5 --- /dev/null +++ b/chart/docker-auth/README.md @@ -0,0 +1,224 @@ +# Helm Chart for docker_auth + +A Helm chart for deploying a [private Docker Registry](https://github.com/cesanta/docker_auth). + +## Overview + +This chart deploys docker_auth, which provides token-based authentication and authorization for Docker Registry v2. It implements the Docker Registry authentication protocol and supports various authentication backends. + +## Prerequisites + +- Kubernetes 1.25+ +- Helm 3.0+ + +## Installation + +### Add Helm Repository + +```bash +helm repo add cesanta https://cesanta.github.io/docker_auth/ +helm repo update +``` + +### Basic Installation + +```bash +helm install my-docker-auth cesanta/docker-auth +``` + +### Installation with Custom Values + +```bash +helm install docker-auth cesanta/docker-auth -f values.yaml +``` + +### Uninstall + +```bash +helm uninstall docker-auth +``` + +## Configuration + +### Values + +| Parameter | Description | Default | +|-----------|-------------|---------| +| **Image** | | | +| `image.repository` | Docker image repository | `cesanta/docker_auth` | +| `image.tag` | Docker image tag | `1.14.0` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| **Deployment** | | | +| `replicaCount` | Number of replicas | `1` | +| `nameOverride` | Override name of the chart | `""` | +| `fullnameOverride` | Override full name of the chart | `""` | +| **Logging** | | | +| `logging.level` | Log verbosity level (0-10). Passed as `--v=X` flag to docker_auth binary. Higher numbers = more verbose logging. | `2` | +| **Authentication** | | | +| `configmap.data.token.issuer` | Token issuer name (must match registry config) | `"Acme auth server"` | +| `configmap.data.token.expiration` | Token expiration time in seconds | `900` | +| `configmap.data.token.disableLegacyKeyId` | Disables legacy key IDs for registry v3 | `false` | +| `configmap.data.users` | Static user definitions | See values.yaml | +| `configmap.data.acl` | Access control list rules | See values.yaml | +| **TLS/Certificates** | | | +| `secret.data.server.certificate` | Server certificate content (PEM format, base64 encoded) | `""` | +| `secret.data.server.key` | Server private key content (PEM format, base64 encoded) | `""` | +| `secret.secretName` | External secret name for certificates (alternative to inline cert/key) | `""` | +| **Service** | | | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Service port | `5001` | +| `service.targetPort` | Container port | `5001` | +| **Ingress** | | | +| `ingress.enabled` | Enable ingress | `true` | +| `ingress.className` | Ingress class name | `""` | +| `ingress.annotations` | Ingress annotations | `{}` | +| `ingress.labels` | Ingress labels | `{}` | +| `ingress.hosts` | Ingress hosts configuration | See values.yaml | +| `ingress.tls` | Ingress TLS configuration | `[]` | +| **Resources** | | | +| `resources` | CPU/Memory resource requests/limits | `{}` | +| `nodeSelector` | Node selector | `{}` | +| `tolerations` | Tolerations | `[]` | +| `affinity` | Affinity rules | `{}` | +| **Security** | | | +| `podSecurityContext` | Pod security context | `{}` | +| `containerSecurityContext` | Container security context | `{}` | +| `podAnnotations` | Pod annotations | `{}` | +| **Registry Integration** | | | +| `registry.enabled` | Enable integrated docker-registry | `false` | + +### Quick Start Example + +```yaml +# values.yaml +ingress: + enabled: true + className: "nginx" + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + hosts: + - host: docker-auth.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: docker-auth-tls + hosts: + - docker-auth.example.com + +configmap: + data: + token: + issuer: "docker-auth-prod" + expiration: 900 + users: + "admin": + password: "$2y$05$..." # Generate with htpasswd -Bbn admin password + acl: + - match: {account: "admin"} + actions: ["*"] + comment: "Admin has full access" + - match: {account: ""} + actions: ["pull"] + comment: "Anonymous users can pull" +``` + +## Certificate Management + +### Generate Self-Signed Certificates + +```bash +openssl req -new -newkey rsa:4096 -days 5000 -nodes -x509 \ + -subj "/C=DE/ST=BW/L=Mannheim/O=ACME/CN=docker-auth" \ + -keyout generated-docker-auth-server.key \ + -out generated-docker-auth-server.pem + +CERT_PEM_BASE64=`cat generated-docker-auth-server.pem | base64` +CERT_KEY_BASE64=`cat generated-docker-auth-server.key | base64` +``` + +## Access Control Lists (ACL) + +### ACL Configuration + +```yaml +configmap: + data: + acl: + - match: { account: "admin" } + actions: ["*"] + comment: "Admin has full access to everything." + - match: { account: "" } + actions: ["pull"] + comment: "Anonymous users can pull" +``` + +## Monitoring and Logging + +### Increase Log Verbosity + +```yaml +logging: + level: 5 # Higher values = more verbose (0-10) +``` + +## Troubleshooting + +### Debug Commands + +```bash +# Check pod logs +kubectl logs -l app.kubernetes.io/name=docker-auth + +# Check configuration +kubectl get configmap docker-auth -o yaml + +# Test authentication endpoint +curl -k https://docker-auth.example.com/auth + +# Verify certificate +openssl x509 -in certificate.pem -text -noout +``` + +## Integration with Docker Registry + +To use with Docker Registry, configure the registry with: + +```yaml +# Registry configuration +auth: + token: + realm: https://docker-auth.example.com/auth + service: token-service + issuer: docker-auth-prod # Must match configmap.data.token.issuer + rootcertbundle: /path/to/docker-auth.crt +``` + +## Development + +### Chart Development + +```bash +# Lint the chart +helm lint chart/docker-auth + +# Test template rendering +helm template test-release chart/docker-auth + +# Package the chart +helm package chart/docker-auth +``` + +### Update Repository + +```bash +cd chart/docker-auth +helm lint +helm package . +mv docker-auth-*.tgz ../../docs/ +helm repo index ../../docs/ --url https://cesanta.github.io/docker_auth/ +git add ../../docs/ +git commit -m "Updated helm repository" +git push origin main +``` diff --git a/chart/docker-auth/templates/NOTES.txt b/chart/docker-auth/templates/NOTES.txt new file mode 100755 index 00000000..42b1f0a0 --- /dev/null +++ b/chart/docker-auth/templates/NOTES.txt @@ -0,0 +1,38 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "docker-auth.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "docker-auth.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "docker-auth.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "docker-auth.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} + +{{- if not .Values.registry.enabled }} +2. Configure your docker registry: +auth: + token: + autoredirect: false +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + {{- range .paths }} + realm: http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $.host }}{{ .path }} + {{- end }} +{{- end }} +{{- end }} + service: token-service + issuer: {{ .Values.configmap.data.token.issuer }} + rootcertbundle: /config/certs/{{ .Values.secret.certificateFileName }} +{{- end }} diff --git a/chart/docker-auth/templates/_helpers.tpl b/chart/docker-auth/templates/_helpers.tpl new file mode 100755 index 00000000..f5b8a530 --- /dev/null +++ b/chart/docker-auth/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "docker-auth.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "docker-auth.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "docker-auth.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/chart/docker-auth/templates/configmap.yaml b/chart/docker-auth/templates/configmap.yaml new file mode 100755 index 00000000..61758178 --- /dev/null +++ b/chart/docker-auth/templates/configmap.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "docker-auth.name" . }} +data: + auth_config.yml: | + server: + addr: ":{{ .Values.service.targetPort }}" + token: + issuer: "{{ .Values.configmap.data.token.issuer }}" # Must match issuer in the Registry config. + expiration: {{ .Values.configmap.data.token.expiration }} +{{- if .Values.secret.secretName }} + certificate: "/config/certs/{{ default "tls.crt" .Values.secret.certificateFileName }}" + key: "/config/certs/{{ default "tls.key" .Values.secret.keyFileName }}" +{{- else }} + certificate: "/config/certs/server.pem" + key: "/config/certs/server.key" +{{- end }} +{{- if .Values.configmap.data.token.disableLegacyKeyId }} + disable_legacy_key_id: {{ .Values.configmap.data.token.disableLegacyKeyId }} +{{- end }} + users: + {{ .Values.configmap.data.users | toYaml | nindent 6 }} + acl: + {{ .Values.configmap.data.acl | toYaml | nindent 6 }} diff --git a/chart/docker-auth/templates/deployment.yaml b/chart/docker-auth/templates/deployment.yaml new file mode 100755 index 00000000..26901b38 --- /dev/null +++ b/chart/docker-auth/templates/deployment.yaml @@ -0,0 +1,87 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "docker-auth.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + helm.sh/chart: {{ include "docker-auth.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if .Values.podAnnotations }} + {{- range $key, $value := .Values.podAnnotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + spec: + {{- if .Values.podSecurityContext }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + {{- if .Values.containerSecurityContext }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + args: ["--v={{ .Values.logging.level }}", "-logtostderr", "/config/auth_config.yml"] + volumeMounts: + - name: {{ include "docker-auth.name" . }}-config + mountPath: /config + - name: {{ include "docker-auth.name" . }}-secret + mountPath: /config/certs + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: {{ include "docker-auth.name" . }} + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.targetPort }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.targetPort }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: {{ include "docker-auth.name" . }}-config + configMap: + name: {{ include "docker-auth.name" . }} + - name: {{ include "docker-auth.name" . }}-secret + secret: +{{- if .Values.secret.secretName }} + secretName: {{ .Values.secret.secretName }} +{{- else }} + secretName: {{ include "docker-auth.name" . }} +{{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/chart/docker-auth/templates/ingress.yaml b/chart/docker-auth/templates/ingress.yaml new file mode 100755 index 00000000..51ce49c2 --- /dev/null +++ b/chart/docker-auth/templates/ingress.yaml @@ -0,0 +1,50 @@ +{{- if .Values.ingress.enabled -}} +{{- $serviceName := include "docker-auth.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "docker-auth.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + helm.sh/chart: {{ include "docker-auth.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ $serviceName }} + port: + number: {{ $servicePort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/chart/docker-auth/templates/secret.yaml b/chart/docker-auth/templates/secret.yaml new file mode 100755 index 00000000..0078d3b8 --- /dev/null +++ b/chart/docker-auth/templates/secret.yaml @@ -0,0 +1,11 @@ +{{- if not .Values.secret.secretName }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "docker-auth.name" . }} +type: Opaque +data: + server.pem: {{ .Values.secret.data.server.certificate | quote }} + server.key: {{ .Values.secret.data.server.key | quote }} +{{- end }} diff --git a/chart/docker-auth/templates/service.yaml b/chart/docker-auth/templates/service.yaml new file mode 100755 index 00000000..a26ec5c9 --- /dev/null +++ b/chart/docker-auth/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "docker-auth.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + helm.sh/chart: {{ include "docker-auth.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/chart/docker-auth/templates/tests/test-connection.yaml b/chart/docker-auth/templates/tests/test-connection.yaml new file mode 100644 index 00000000..9ae8b172 --- /dev/null +++ b/chart/docker-auth/templates/tests/test-connection.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "docker-auth.fullname" . }}-test-connection" + labels: + app.kubernetes.io/name: {{ include "docker-auth.name" . }} + helm.sh/chart: {{ include "docker-auth.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "docker-auth.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/chart/docker-auth/values.yaml b/chart/docker-auth/values.yaml new file mode 100644 index 00000000..1da990ba --- /dev/null +++ b/chart/docker-auth/values.yaml @@ -0,0 +1,94 @@ +# Default values for docker-auth. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: cesanta/docker_auth + tag: 1.14.0 + pullPolicy: IfNotPresent + +registry: + enabled: false + +logging: + level: 2 + +secret: + data: + server: + certificate: "" + key: "" +configmap: + data: + token: + issuer: "Acme auth server" + expiration: 900 + disableLegacyKeyId: false + users: + "admin": + password: "$2y$05$LO.vzwpWC5LZGqThvEfznu8qhb5SGqvBSWY1J3yZ4AxtMRZ3kN5jC" # password: badmin + "test": + password: "$2y$05$WuwBasGDAgr.QCbGIjKJaep4dhxeai9gNZdmBnQXqpKly57oNutya" # password: 123 + acl: + - match: {account: "admin"} + actions: ["*"] + comment: "Admin has full access to everything." + - match: {account: "test"} + actions: ["pull"] + comment: "User \"test\" can pull stuff." + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 5001 + targetPort: 5001 + +ingress: + enabled: true + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + labels: {} + hosts: + - host: docker-auth.test.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# podAnnotations to use for the deployment. Optional +podAnnotations: {} + +# SecurityContext at container level to use for the deployment. Optional +containerSecurityContext: {} + +# SecurityContext at pod level to use for the deployment. Optional +podSecurityContext: {} + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..226c73ea --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Helm repo for Docker Auth + +Documentation available here: diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..c7418817 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/docs/auth-methods.md b/docs/auth-methods.md index f95f4025..1cc67572 100644 --- a/docs/auth-methods.md +++ b/docs/auth-methods.md @@ -13,7 +13,10 @@ github_auth: organization: "my-org-name" client_id: "..." client_secret: "..." # or client_secret_file - token_db: /data/tokens.db + level_token_db: + path: /data/tokens.db + # Optional token hash cost for bcrypt hashing + # token_hash_cost: 5 ``` Then specify what teams can do via acls diff --git a/docs/casbin_backend.md b/docs/casbin_backend.md new file mode 100644 index 00000000..44dfe4d6 --- /dev/null +++ b/docs/casbin_backend.md @@ -0,0 +1,15 @@ +# Casbin Backend + +[Casbin](https://github.com/casbin/casbin) is a powerful and efficient open-source access control library written by Golang. It provides support for enforcing authorization based on various access control models. + +## Usage + +add casbin section in yml configuration file + +```yaml +casbin_authz: + model_path: "path/to/model" + policy_path: "path/to/policy" +``` + +more info see: https://github.com/casbin/casbin \ No newline at end of file diff --git a/docs/docker-auth-1.0.3.tgz b/docs/docker-auth-1.0.3.tgz new file mode 100644 index 00000000..03b89f7f Binary files /dev/null and b/docs/docker-auth-1.0.3.tgz differ diff --git a/docs/docker-auth-1.1.0.tgz b/docs/docker-auth-1.1.0.tgz new file mode 100644 index 00000000..4c20b0d7 Binary files /dev/null and b/docs/docker-auth-1.1.0.tgz differ diff --git a/docs/docker-auth-1.1.1.tgz b/docs/docker-auth-1.1.1.tgz new file mode 100644 index 00000000..52bc7a18 Binary files /dev/null and b/docs/docker-auth-1.1.1.tgz differ diff --git a/docs/docker-auth-1.14.0.tgz b/docs/docker-auth-1.14.0.tgz new file mode 100644 index 00000000..f20de6f8 Binary files /dev/null and b/docs/docker-auth-1.14.0.tgz differ diff --git a/docs/docker-auth-1.2.0.tgz b/docs/docker-auth-1.2.0.tgz new file mode 100644 index 00000000..ea514476 Binary files /dev/null and b/docs/docker-auth-1.2.0.tgz differ diff --git a/docs/docker-auth-1.3.0.tgz b/docs/docker-auth-1.3.0.tgz new file mode 100644 index 00000000..b34741e9 Binary files /dev/null and b/docs/docker-auth-1.3.0.tgz differ diff --git a/docs/docker-auth-1.4.0.tgz b/docs/docker-auth-1.4.0.tgz new file mode 100644 index 00000000..e2646706 Binary files /dev/null and b/docs/docker-auth-1.4.0.tgz differ diff --git a/docs/docker-auth-1.5.0.tgz b/docs/docker-auth-1.5.0.tgz new file mode 100644 index 00000000..38b1ea14 Binary files /dev/null and b/docs/docker-auth-1.5.0.tgz differ diff --git a/docs/index.yaml b/docs/index.yaml new file mode 100644 index 00000000..c3ec649c --- /dev/null +++ b/docs/index.yaml @@ -0,0 +1,191 @@ +apiVersion: v1 +entries: + docker-auth: + - apiVersion: v2 + appVersion: 1.14.0 + created: "2025-06-19T22:33:35.442011-04:00" + description: Docker Registry V2 authentication server + digest: 1f968367c9fbd8e3b322c0986fc501d3827f180c5f81969646220ffd262fec9b + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + kubeVersion: '>=1.25' + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + - email: hello@techknowlogick.com + name: techknowlogick + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.14.0.tgz + version: 1.14.0 + - apiVersion: v2 + appVersion: 1.11.0 + created: "2025-06-19T22:33:35.44474-04:00" + description: Docker Registry V2 authentication server + digest: f62885093fb8aa931dfb05273e9f9429d8c0aba8f7800e9b3c8bdb605bd6842b + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.5.0.tgz + version: 1.5.0 + - apiVersion: v2 + appVersion: 1.8.0 + created: "2025-06-19T22:33:35.444085-04:00" + description: Docker Registry V2 authentication server + digest: ce41bfe9b4ddd392e67f338a95587169225e0fdc7e62258cbf7b1881c09c2d22 + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.4.0.tgz + version: 1.4.0 + - apiVersion: v2 + appVersion: 1.8.0 + created: "2025-06-19T22:33:35.443038-04:00" + description: Docker Registry V2 authentication server + digest: f84d570ee0a2d37ef5838ec51af7d43a24810e8f7d6c423f87c6c046bf4d286a + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.3.0.tgz + version: 1.3.0 + - apiVersion: v2 + appVersion: 1.8.0 + created: "2025-06-19T22:33:35.442561-04:00" + description: Docker Registry V2 authentication server + digest: b656a46edc33434add27757dde85243909b4675c608c7b86f033de9f21faca8b + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.2.0.tgz + version: 1.2.0 + - apiVersion: v2 + appVersion: 1.7.0 + created: "2025-06-19T22:33:35.441554-04:00" + description: Docker Registry V2 authentication server + digest: add7b754e9b8ff9f0b9e839c0759aebb3e6a59f0db91a111b5c92009a5bd1ab6 + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.1.1.tgz + version: 1.1.1 + - apiVersion: v2 + appVersion: 1.7.0 + created: "2025-06-19T22:33:35.440926-04:00" + description: Docker Registry V2 authentication server + digest: e20ff37b10dcfaa3b7cfcf78d6b9bdabfb5b1bfcdb2384fcd5ba99459297df56 + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.1.0.tgz + version: 1.1.0 + - apiVersion: v2 + appVersion: 1.4.0 + created: "2025-06-19T22:33:35.440196-04:00" + description: Docker Registry V2 authentication server + digest: 7842a1c2672bb63393cec4df2ec6f554d114e32dcc91ced98e427f365be423c7 + home: https://github.com/cesanta/docker_auth + keywords: + - docker + - registry + - docker-auth + - docker-registry + - token + maintainers: + - email: 1294057873@qq.com + name: duyanghao + - email: github@farberg.de + name: pfisterer + name: docker-auth + sources: + - https://github.com/cesanta/docker_auth + urls: + - https://cesanta.github.io/docker_auth/docker-auth-1.0.3.tgz + version: 1.0.3 +generated: "2025-06-19T22:33:35.439234-04:00" diff --git a/examples/casbin_authz_model.conf b/examples/casbin_authz_model.conf new file mode 100644 index 00000000..17de928f --- /dev/null +++ b/examples/casbin_authz_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = account, type, name, service, ip, action, labels + +[policy_definition] +p = account, type, name, service, ip, action, labels + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.account == "admin" || (g(r.account, p.account) && r.type == p.type && r.name == p.name && r.service == p.service && ipMatch(r.ip, p.ip) && r.action == p.action && labelMatch(r.labels, p.labels)) \ No newline at end of file diff --git a/examples/casbin_authz_policy.csv b/examples/casbin_authz_policy.csv new file mode 100644 index 00000000..e7d4b976 --- /dev/null +++ b/examples/casbin_authz_policy.csv @@ -0,0 +1,5 @@ +p, alice, book, book1, bookstore1, 1.2.3.4, read, "{""a"":[""b""]}" +p, alice, book, book1, bookstore1, 1.2.3.4, write, "{""a"":[""b""]}" +p, role1, book, book2, bookstore1, 192.168.1.0/24, read, "{""a"":[""b"",""c""]}" + +g, bob, role1 \ No newline at end of file diff --git a/examples/ldap_auth.yml b/examples/ldap_auth.yml index aee07e4e..ab540d0d 100644 --- a/examples/ldap_auth.yml +++ b/examples/ldap_auth.yml @@ -24,6 +24,11 @@ ldap_auth: # specify them here. Plain text password is read from the file. bind_dn: bind_password_file: + # If the auth request credentials shall be used for the initial LDAP bind, + # set this to true and refer to ${account} in the bind_dn field. + # The bind_password_file setting is ignored in this case. + initial_bind_as_user: true + bind_dn: "cn=${account},cn=users,dc=example,dc=com" # User query settings. ${account} is expanded from auth request base: o=example.com filter: (&(uid=${account})(objectClass=person)) diff --git a/examples/reference.yml b/examples/reference.yml index e49362a5..31de9ee7 100644 --- a/examples/reference.yml +++ b/examples/reference.yml @@ -12,8 +12,12 @@ server: # Server settings. # Address to listen on. + # Can be HOST:PORT for TCP or file path (e.g. /run/docker_auth.sock) for Unix socket. addr: ":5001" + # Network, can be "tcp" or "unix" ("tcp" if unspecified). + net: "tcp" + # URL path prefix to use. path_prefix: "" @@ -22,6 +26,35 @@ server: # Server settings. # Use specific certificate and key. certificate: "/path/to/server.pem" key: "/path/to/server.key" + # + # The following optional settings will fine tune TLS configuration to improve security. + # Leaving them unset should be just fine for most installations. + # + # Enable HTTP Strict Transport Security. + # hsts: true + # + # Set minimum TLS version. + # Values can be found at https://golang.org/pkg/crypto/tls/#pkg-constants + # Either the version name (i.e. TLS11) or its uint16 value can be specified. + # tls_min_version: TLS12 + # + # List of TLS curve preferences. + # Values can be found at https://golang.org/pkg/crypto/tls/#CurveID + # Either CurveID names (i.e. P384) or uint16 values can be specified. + # tls_curve_preferences: + # - P521 + # - 24 + # - P256 + # + # List of enabled TLS cipher suites. + # Values can be found at https://golang.org/pkg/crypto/tls/#pkg-constants + # Either CipherSuite names (i.e. TLS_RSA_WITH_RC4_128_SHA) or uint16 values can be specified. + # tls_cipher_suites: + # - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + # - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + # - 0xc014 + # - 0xc00a + # Use LetsEncrypt (https://letsencrypt.org/) to automatically obtain and maintain a certificate. # Note that this only applies to server TLS certificate, this certificate will not be used for tokens letsencrypt: @@ -51,6 +84,11 @@ token: # Settings for the tokens. # If not specified, server's TLS certificate and key are used. # certificate: "..." # key: "..." + # Whether the legacy libtrust key ID should be embedded in the `kid` header of the token. + # Set this to true if you are using registry v3, and to false if you are using registry v2. + # If set to true, the JWK thumbprint (see https://www.rfc-editor.org/rfc/rfc7638.html) of the certificate will be embedded instead. + # Defaults to false. + # disable_legacy_key_id: false # Authentication methods. All are tried, any one returning success is sufficient. # At least one must be configured. If you want an unauthenticated public setup, @@ -82,9 +120,12 @@ google_auth: # client_secret: "verysecret" client_secret_file: "/path/to/client_secret.txt" # Where to store server tokens. Required. - token_db: "/somewhere/to/put/google_tokens.ldb" + level_token_db: + path: "/somewhere/to/put/google_tokens.ldb" + # Optional token hash cost for bcrypt hashing + # token_hash_cost: 5 # How long to wait when talking to Google servers. Optional. - http_timeout: 10 + http_timeout: "10s" # GitHub authentication. # ==! NB: DO NOT ENTER YOUR GITHUB PASSWORD AT "docker login". IT WILL NOT WORK. @@ -102,12 +143,23 @@ github_auth: # want to have sensitive information checked in. # client_secret: "verysecret" client_secret_file: "/path/to/client_secret.txt" - # Either token_db file for storing of server tokens. - token_db: "/somewhere/to/put/github_tokens.ldb" - # or google cloud storage for storing of the sensitive information. - gcs_token_db: + # Either level_token_db file for storing of server tokens. + level_token_db: + path: "/somewhere/to/put/github_tokens.ldb" + # Optional token hash cost for bcrypt hashing + # token_hash_cost: 5 + # or google cloud storage for storing of the sensitive information, + gcs_token_db: bucket: "tokenBucket" client_secret_file: "/path/to/client_secret.json" + # or Redis, + redis_token_db: + redis_options: + # with a single instance, + addr: localhost:6379 + redis_cluster_options: + # or in the cluster mode. + addrs: ["localhost:7000"] # How long to wait when talking to GitHub servers. Optional. http_timeout: "10s" # How long to wait before revalidating the GitHub token. Optional. @@ -117,10 +169,93 @@ github_auth: github_web_uri: "https://github.acme.com/" # The Github API URI in case you are using Github Enterprise. # Includes the protocol, without trailing slash. - defaults to: https://api.github.com - github_api_uri: "https://github.acme.com/api/v3" + github_api_uri: "https://github.acme.com/api/v3" # Set an URL to display in the `docker login` command when succesfully authenticated. Optional. registry_url: localhost:5000 +# OpenID Connect authentication +# ==! NB: DO NOT ENTER YOUR OIDC PASSWORD AT "docker login". IT WILL NOT WORK. +# Instead, Auth server maintains a database of OIDC authentication tokens. +# Go to the server's port as HTTPS with your browser and follow the "Login with OIDC account" link. +# Once signed in, you will get a throw-away password which you can use for Docker login. +oidc_auth: + # --- required --- + # The issuer URL of your OIDC provider. It has to be extendable with /.well-known/openid-configuration to request all + # OIDC endpoints for token and authorization requests + issuer: "my_issuer_url" + # The redirect URI which is registered for this client at your OIDC provider. It has to end with /oidc_auth. + redirect_url: "my_redirect_uri/oidc_auth" + # The client id and client secret of the client that is registered at your OIDC provider for docker_auth + client_id: "be4ut1fu1-cl13n7-1d" + client_secret: "be4ut1fu1-cl13n7-s3cr37" + # you can also give the client_secret in a file. Either a client_secret or a client_secret_file has to be provided + # client_secret_file: "/path/to/client_secret.txt" + # + # a file in which the tokens should be stored. Does not have to exist, it will be generated in this case + level_token_db: + path: "/path/to/tokens.ldb" + # Optional token hash cost for bcrypt hashing + # token_hash_cost: 5 + # --- optional --- + # How long to wait when talking to the OIDC provider. + http_timeout: "10s" + # the url of the registry where you want to login. Is used to present the full docker login command. + registry_url: "url_of_my_beautiful_docker_registry" + # The claim to use for the username. + # Default: email + user_claim: email + # String array claims that will be used as labels. + label_claims: + - groups + # Default: [openid, email] + scopes: + - openid + - email + + +# Gitlab authentication. +# ==! NB: DO NOT ENTER YOUR Gitlab PASSWORD AT "docker login". IT WILL NOT WORK. +# Instead, Auth server maintains a database of Gitlab authentication tokens. +# Go to the server's port as HTTPS with your browser and follow the "Login with Gitlab account" link. +# Once signed in, you will get a throw-away password which you can use for Docker login. +gitlab_auth: + client_id: "1223123456" + # Either client_secret or client_secret_file is required. Use client_secret_file if you don't + # want to have sensitive information checked in. + # client_secret: "verysecret" + client_secret_file: "/path/to/client_secret.txt" + # Either level_token_db file for storing of server tokens. + level_token_db: + path: "/somewhere/to/put/gitlab_tokens.ldb" + # Optional token hash cost for bcrypt hashing + # token_hash_cost: 5 + # or google cloud storage for storing of the sensitive information, + gcs_token_db: + bucket: "tokenBucket" + client_secret_file: "/path/to/client_secret.json" + # or Redis, + redis_token_db: + redis_options: + # with a single instance, + addr: localhost:6379 + redis_cluster_options: + # or in the cluster mode. + addrs: ["localhost:7000"] + # How long to wait when talking to GitLab servers. Optional. + http_timeout: "10s" + # How long to wait before revalidating the Gitlab token. Optional. + revalidate_after: "1h" + # Includes the protocol, without trailing slash. Optional - defaults to: https://gitlab.com + gitlab_web_uri: "https://gitlab.com/" + # Includes the protocol, without trailing slash. - defaults to: https://gitlab.com/api/v4 + gitlab_api_uri: "https://gitlab.com/api/v4" + # Set an URL to display in the `docker login` command when successfully authenticated. Optional. + registry_url: localhost:5000 + # grant_type is used for the authentication purpose. Required. + grant_type: "authorization_code" + # Redirect uri is used for the authentication purpose. Must end with '/gitlab_auth' prefix. Required. + redirect_uri: "https://localhost:5001/gitlab_auth" + # LDAP authentication. # Authentication is performed by first binding to the server, looking up the user entry # by using the specified filter, and then re-binding using the matched DN and the password provided. @@ -140,7 +275,7 @@ ldap_auth: # specify them here. Plain text password is read from the file. bind_dn: bind_password_file: - # User query settings. ${account} is expanded from auth request + # User query settings. ${account} is expanded from auth request base: o=example.com filter: (&(uid=${account})(objectClass=person)) # Labels can be mapped from LDAP attributes @@ -153,6 +288,8 @@ ldap_auth: attribute: memberOf # Special handling to simplify the values to just the common name parse_cn: true + # lower case the value + lower_case: true mongo_auth: # Essentially all options are described here: https://godoc.org/gopkg.in/mgo.v2#DialInfo @@ -177,6 +314,12 @@ mongo_auth: # Unlike acl_mongo we don't cache the full user set. We just query mongo for # an exact match for each authorization +xorm_auth: + # the database type you'd like to connect to + database_type: "mysql" + # the connection string to connect to the database + conn_string: "username:password@/database_name?charset=utf8" + # External authentication - call an external progam to authenticate user. # Username and password are passed to command's stdin and exit code is examined. # 0 - allow, 1 - deny, 2 - no match, other - error. @@ -308,6 +451,18 @@ acl_mongo: # (See https://golang.org/pkg/time/#ParseDuration for a format description.) cache_ttl: "1m" +# (optional) Define to query ACL from a XORM.io database connection. +acl_xorm: + # the database type you'd like to connect to + database_type: "mysql" + conn_string: "username:password@/database_name?charset=utf8" + cache_ttl: "1m" + +# (optioinal) Use casbin to verify permission +casbin_authz: + model_path: "path/to/model" + policy_path: "path/to/csv" + # External authorization - call an external progam to authorize user. # JSON of authz.AuthRequestInfo is passed to command's stdin and exit code is examined. # 0 - allow, 1 - deny, other - error. @@ -320,3 +475,4 @@ ext_authz: # return the set of authorized actions is the user is authorized. Otherwise return nil plugin_authz: plugin_path: "" + diff --git a/examples/simple.yml b/examples/simple.yml index bebe5924..b307e0ab 100644 --- a/examples/simple.yml +++ b/examples/simple.yml @@ -1,4 +1,4 @@ -# A simple example. See reference.yml for explanation for explanation of all options. +# A simple example. See reference.yml for explanation of all options. # # auth: # token: @@ -15,6 +15,8 @@ server: token: issuer: "Acme auth server" # Must match issuer in the Registry config. expiration: 900 + # Uncomment the following line if you are using registry v3, leave it commented if you are using registry v2 + # disable_legacy_key_id: true users: # Password is specified as a BCrypt hash. Use `htpasswd -nB USERNAME` to generate. @@ -27,7 +29,7 @@ acl: - match: {account: "admin"} actions: ["*"] comment: "Admin has full access to everything." - - match: {account: "user"} + - match: {account: "test"} actions: ["pull"] - comment: "User \"user\" can pull stuff." + comment: "User \"test\" can pull stuff." # Access is denied by default.