diff --git a/.dockerignore b/.dockerignore index 31b16d2aab..ec905b5eb8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,2 @@ -.github/ -.gitpod.yml bin/ tmp/ diff --git a/.editorconfig b/.editorconfig index 27917441d8..085f9d0e31 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,3 +19,6 @@ indent_style = tab [{config.yaml.dist,config.dev.yaml}] indent_size = 2 + +[.golangci.yaml] +indent_size = 2 diff --git a/.envrc b/.envrc index 5817bffc67..fb06536f72 100644 --- a/.envrc +++ b/.envrc @@ -1,6 +1,6 @@ -if ! has nix_direnv_version || ! nix_direnv_version 1.5.0; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.5.0/direnvrc" "sha256-carKk9aUFHMuHt+IWh74hFj58nY4K3uywpZbwXX0BTI=" +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" fi -use flake +use flake . --impure dotenv_if_exists diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 64156bfa7a..7b8f9b7e2e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,9 @@ blank_issues_enabled: false contact_links: + - name: 📖 Documentation enhancement + url: https://github.com/dexidp/website/issues + about: Suggest an improvement to the documentation + - name: ❓ Ask a question url: https://github.com/dexidp/dex/discussions/new?category=q-a about: Ask and discuss questions with other Dex community members @@ -13,5 +17,5 @@ contact_links: about: Please ask and answer questions here - name: 💡 Dex Enhancement Proposal - url: https://github.com/dexidp/dex/tree/master/enhancements/README.md + url: https://github.com/dexidp/dex/tree/master/docs/enhancements/README.md about: Open a proposal for significant architectural change diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bcaee00ae3..a706b551a1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,15 +21,3 @@ Thank you for sending a pull request! Here are some tips for contributors: --> #### Special notes for your reviewer - -#### Does this PR introduce a user-facing change? - - - -```release-note - -``` diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 9decd34e3e..eab38858be 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -11,10 +11,10 @@ to confirm receipt of the issue. ## Review Process Once a maintainer has confirmed the relevance of the report, a draft security -advisory will be created on Github. The draft advisory will be used to discuss +advisory will be created on GitHub. The draft advisory will be used to discuss the issue with maintainers, the reporter(s). If the reporter(s) wishes to participate in this discussion, then provide -reporter Github username(s) to be invited to the discussion. If the reporter(s) +reporter GitHub username(s) to be invited to the discussion. If the reporter(s) does not wish to participate directly in the discussion, then the reporter(s) can request to be updated regularly via email. diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index b3129d93cf..f66cc18740 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -7,6 +7,10 @@ updates: - "area/dependencies" schedule: interval: "daily" + groups: + etcd: + patterns: + - "go.etcd.io/*" - package-ecosystem: "gomod" directory: "/api/v2" @@ -15,6 +19,13 @@ updates: schedule: interval: "daily" + - package-ecosystem: "gomod" + directory: "/examples" + labels: + - "area/dependencies" + schedule: + interval: "daily" + - package-ecosystem: "docker" directory: "/" labels: diff --git a/.github/workflows/analysis-scorecard.yaml b/.github/workflows/analysis-scorecard.yaml new file mode 100644 index 0000000000..258ec01bc8 --- /dev/null +++ b/.github/workflows/analysis-scorecard.yaml @@ -0,0 +1,47 @@ +name: OpenSSF Scorecard + +on: + branch_protection_rule: + push: + branches: [ main ] + schedule: + - cron: '30 0 * * 5' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + id-token: write + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload results as artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: OpenSSF Scorecard results + path: results.sarif + retention-days: 5 + + - name: Upload results to GitHub Security tab + uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5 + with: + sarif_file: results.sarif diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml index 0237b3ac66..4c2e74f132 100644 --- a/.github/workflows/artifacts.yaml +++ b/.github/workflows/artifacts.yaml @@ -1,12 +1,31 @@ name: Artifacts on: - push: - branches: - - master - tags: - - v[0-9]+.[0-9]+.[0-9]+ - pull_request: + workflow_call: + inputs: + publish: + description: Publish artifacts to the artifact store + default: false + required: false + type: boolean + secrets: + DOCKER_USERNAME: + required: true + DOCKER_PASSWORD: + required: true + outputs: + container-image-name: + description: Container image name + value: ${{ jobs.container-images.outputs.name }} + container-image-digest: + description: Container image digest + value: ${{ jobs.container-images.outputs.digest }} + container-image-ref: + description: Container image ref + value: ${{ jobs.container-images.outputs.ref }} + +permissions: + contents: read jobs: container-images: @@ -18,80 +37,233 @@ jobs: - alpine - distroless + permissions: + attestations: write + contents: read + packages: write + id-token: write + security-events: write + + outputs: + name: ${{ steps.image-name.outputs.value }} + digest: ${{ steps.build.outputs.digest }} + ref: ${{ steps.image-ref.outputs.value }} + steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-tags: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Set up Syft + uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1 + + - name: Install cosign + uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 + + - name: Set image name + id: image-name + run: echo "value=ghcr.io/${{ github.repository }}" >> "$GITHUB_OUTPUT" - - name: Gather metadata + - name: Gather build metadata id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: | - ghcr.io/dexidp/dex - dexidp/dex + ${{ steps.image-name.outputs.value }} + ${{ github.repository == 'dexidp/dex' && 'dexidp/dex' || '' }} flavor: | latest = false tags: | type=ref,event=branch,enable=${{ matrix.variant == 'alpine' }} - type=ref,event=pr,enable=${{ matrix.variant == 'alpine' }} + type=ref,event=pr,prefix=pr-,enable=${{ matrix.variant == 'alpine' }} type=semver,pattern={{raw}},enable=${{ matrix.variant == 'alpine' }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && matrix.variant == 'alpine' }} + type=raw,value=latest,enable=${{ github.ref_name == github.event.repository.default_branch && matrix.variant == 'alpine' }} type=ref,event=branch,suffix=-${{ matrix.variant }} - type=ref,event=pr,suffix=-${{ matrix.variant }} + type=ref,event=pr,prefix=pr-,suffix=-${{ matrix.variant }} type=semver,pattern={{raw}},suffix=-${{ matrix.variant }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }},suffix=-${{ matrix.variant }} + type=raw,value=latest,enable={{is_default_branch}},suffix=-${{ matrix.variant }} labels: | org.opencontainers.image.documentation=https://dexidp.io/docs/ - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: all + # Multiple exporters are not supported yet + # See https://github.com/moby/buildkit/pull/2760 + - name: Get version from git-version script + id: version + run: echo "value=$(bash ./scripts/git-version)" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + # Multiple exporters are not supported yet + # See https://github.com/moby/buildkit/pull/2760 + - name: Determine build output + uses: haya14busa/action-cond@94f77f7a80cd666cb3155084e428254fea4281fd # v1.2.1 + id: build-output + with: + cond: ${{ inputs.publish }} + if_true: type=image,push=true + if_false: type=oci,dest=image.tar - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io - username: ${{ github.repository_owner }} + username: ${{ github.actor }} password: ${{ github.token }} - if: github.event_name == 'push' + if: inputs.publish - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - if: github.event_name == 'push' + if: inputs.publish - - name: Build and push - uses: docker/build-push-action@v3 + - name: Build and push image + id: build + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le - # cache-from: type=gha - # cache-to: type=gha,mode=max - push: ${{ github.event_name == 'push' }} + platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x tags: ${{ steps.meta.outputs.tags }} build-args: | BASE_IMAGE=${{ matrix.variant }} - VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + VERSION=${{ steps.version.outputs.value }} COMMIT_HASH=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} - labels: ${{ steps.meta.outputs.labels }} + labels: | + ${{ steps.meta.outputs.labels }} + # cache-from: type=gha + # cache-to: type=gha,mode=max + outputs: ${{ steps.build-output.outputs.value }} + # push: ${{ inputs.publish }} + + - name: Sign the images with GitHub OIDC Token + run: | + cosign sign --yes ${{ steps.image-name.outputs.value }}@${{ steps.build.outputs.digest }} + if: inputs.publish + + - name: Set image ref + id: image-ref + run: echo "value=${{ steps.image-name.outputs.value }}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT" + + - name: Fetch image + run: skopeo --insecure-policy copy docker://${{ steps.image-ref.outputs.value }} oci-archive:image.tar + if: inputs.publish + + # Uncomment the following lines for debugging: + # - name: Upload image as artifact + # uses: actions/upload-artifact@v3 + # with: + # name: "[${{ github.job }}] OCI tarball" + # path: image.tar + + - name: Extract OCI tarball + id: extract-oci + run: | + mkdir -p image + tar -xf image.tar -C image + + image_name=$(jq -r '.manifests[0].annotations["io.containerd.image.name"]' image/index.json) + image_tag=$(jq -r '.manifests[0].annotations["org.opencontainers.image.ref.name"]' image/index.json) + + echo "Copying $image_tag -> $image_name" + skopeo copy "oci:image:$image_tag" "docker-daemon:$image_name" + + echo "value=$image_name" >> "$GITHUB_OUTPUT" + if: ${{ !inputs.publish }} + + + # - name: List tags + # run: skopeo --insecure-policy list-tags oci:image + # + # # See https://github.com/anchore/syft/issues/1545 + # - name: Extract image from multi-arch image + # run: skopeo --override-os linux --override-arch amd64 --insecure-policy copy oci:image:${{ steps.image-name.outputs.value }}:${{ steps.meta.outputs.version }} docker-archive:docker.tar + # + # - name: Generate SBOM + # run: syft -o spdx-json=sbom-spdx.json docker-archive:docker.tar + # + # - name: Upload SBOM as artifact + # uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + # with: + # name: "[${{ github.job }}] SBOM" + # path: sbom-spdx.json + # retention-days: 5 + + # TODO: uncomment when the action is working for non ghcr.io pushes. GH Issue: https://github.com/actions/attest-build-provenance/issues/80 + # - name: Generate build provenance attestation + # uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + # with: + # subject-name: dexidp/dex + # subject-digest: ${{ steps.build.outputs.digest }} + # push-to-registry: true + + - name: Generate build provenance attestation + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ghcr.io/${{ github.repository }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + if: inputs.publish + + - name: Prepare image fs for scanning + run: | + image_ref=${{ steps.extract-oci.outputs.value != '' && steps.extract-oci.outputs.value || steps.image-ref.outputs.value }} + docker export $(docker create --rm $image_ref) -o docker-image.tar + + mkdir -p docker-image + tar -xf docker-image.tar -C docker-image + + ## Use cache for the trivy-db to avoid the TOOMANYREQUESTS error https://github.com/aquasecurity/trivy-action/pull/397 + ## To avoid the trivy-db becoming outdated, we save the cache for one day + - name: Get data + id: date + run: echo "date=$(date +%Y-%m-%d)" >> $GITHUB_OUTPUT + + - name: Restore trivy cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: cache/db + key: trivy-cache-${{ steps.date.outputs.date }} + restore-keys: trivy-cache- - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.7.1 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + with: + input: docker-image + format: sarif + output: trivy-results.sarif + scan-type: "rootfs" + scan-ref: "." + cache-dir: "./cache" + # Disable skipping trivy cache for now + env: + TRIVY_SKIP_DB_UPDATE: true + TRIVY_SKIP_JAVA_DB_UPDATE: true + + ## Trivy-db uses `0600` permissions. + ## But `action/cache` use `runner` user by default + ## So we need to change the permissions before caching the database. + - name: change permissions for trivy.db + run: sudo chmod 0644 ./cache/db/trivy.db + + - name: Check Trivy sarif + run: cat trivy-results.sarif + + - name: Upload Trivy scan results as artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - image-ref: "ghcr.io/dexidp/dex:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}" - format: "sarif" - output: "trivy-results.sarif" - if: github.event_name == 'push' + name: "[${{ github.job }}] Trivy scan results" + path: trivy-results.sarif + retention-days: 5 + overwrite: true - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5 with: - sarif_file: "trivy-results.sarif" - if: github.event_name == 'push' + sarif_file: trivy-results.sarif diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index c7eb4ea73c..4aec25e023 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -4,14 +4,19 @@ on: pull_request: types: [opened, labeled, unlabeled, synchronize] +permissions: + contents: read + jobs: release-label: name: Release note label runs-on: ubuntu-latest + if: github.repository == 'dexidp/dex' + steps: - name: Check minimum labels - uses: mheap/github-action-required-labels@v2 + uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5 with: mode: minimum count: 1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 192a44046e..5224e1b368 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,26 +2,30 @@ name: CI on: push: - branches: - - master + branches: [master] pull_request: +permissions: + contents: read + jobs: - build: - name: Build + test: + name: Test runs-on: ubuntu-latest - env: - GOFLAGS: -mod=readonly services: postgres: image: postgres:10.8 + env: + TZ: UTC ports: - 5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 postgres-ent: image: postgres:10.8 + env: + TZ: UTC ports: - 5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 @@ -44,6 +48,24 @@ jobs: - 3306 options: --health-cmd "mysql -proot -e \"show databases;\"" --health-interval 10s --health-timeout 5s --health-retries 5 + mysql8: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: dex + ports: + - 3306 + options: --health-cmd "mysql -proot -e \"show databases;\"" --health-interval 10s --health-timeout 5s --health-retries 5 + + mysql8-ent: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: dex + ports: + - 3306 + options: --health-cmd "mysql -proot -e \"show databases;\"" --health-interval 10s --health-timeout 5s --health-retries 5 + etcd: image: gcr.io/etcd-development/etcd:v3.5.0 ports: @@ -60,26 +82,50 @@ jobs: - 35357 options: --health-cmd "curl --fail http://localhost:5000/v3" --health-interval 10s --health-timeout 5s --health-retries 5 + vault: + image: hashicorp/vault:1.21 + ports: + - 8200 + env: + VAULT_DEV_ROOT_TOKEN_ID: root-token + VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200" + options: --health-cmd "vault status -address=http://localhost:8200 || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 + + openbao: + image: quay.io/openbao/openbao:2.5 + ports: + - 8210 + env: + BAO_DEV_ROOT_TOKEN_ID: root-token + BAO_DEV_LISTEN_ADDRESS: "0.0.0.0:8210" + options: --health-cmd "bao status -address=http://localhost:8210 || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: - go-version: 1.18 + go-version: "1.25" + + - name: Download tool dependencies + run: make deps - - name: Checkout code - uses: actions/checkout@v3 + # Ensure that generated files were committed. + # It can help us determine, that the code is in the intermediate state, which should not be tested. + # Thus, heavy jobs like creating a kind cluster and testing / linting will be skipped. + - name: Verify + run: make verify - name: Start services - run: docker-compose -f docker-compose.test.yaml up -d + run: docker compose -f docker-compose.test.yaml up -d - name: Create kind cluster - uses: helm/kind-action@v1.3.0 + uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0 with: - version: v0.11.1 - node_image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729 - - - name: Download tool dependencies - run: make deps + version: "v0.17.0" + node_image: "kindest/node:v1.25.3@sha256:cd248d1438192f7814fbca8fede13cfe5b9918746dfa12583976158a834fd5c5" - name: Test run: make testall @@ -96,6 +142,18 @@ jobs: DEX_MYSQL_ENT_HOST: 127.0.0.1 DEX_MYSQL_ENT_PORT: ${{ job.services.mysql-ent.ports[3306] }} + DEX_MYSQL8_DATABASE: dex + DEX_MYSQL8_USER: root + DEX_MYSQL8_PASSWORD: root + DEX_MYSQL8_HOST: 127.0.0.1 + DEX_MYSQL8_PORT: ${{ job.services.mysql8.ports[3306] }} + + DEX_MYSQL8_ENT_DATABASE: dex + DEX_MYSQL8_ENT_USER: root + DEX_MYSQL8_ENT_PASSWORD: root + DEX_MYSQL8_ENT_HOST: 127.0.0.1 + DEX_MYSQL8_ENT_PORT: ${{ job.services.mysql8-ent.ports[3306] }} + DEX_POSTGRES_DATABASE: postgres DEX_POSTGRES_USER: postgres DEX_POSTGRES_PASSWORD: postgres @@ -111,19 +169,63 @@ jobs: DEX_ETCD_ENDPOINTS: http://localhost:${{ job.services.etcd.ports[2379] }} DEX_LDAP_HOST: localhost - DEX_LDAP_PORT: 389 - DEX_LDAP_TLS_PORT: 636 + DEX_LDAP_PORT: 3890 + DEX_LDAP_TLS_PORT: 6360 DEX_KEYSTONE_URL: http://localhost:${{ job.services.keystone.ports[5000] }} DEX_KEYSTONE_ADMIN_URL: http://localhost:${{ job.services.keystone.ports[35357] }} DEX_KEYSTONE_ADMIN_USER: demo DEX_KEYSTONE_ADMIN_PASS: DEMO_PASS + DEX_VAULT_ADDR: http://localhost:${{ job.services.vault.ports[8200] }} + DEX_VAULT_TOKEN: root-token + DEX_OPENBAO_ADDR: http://localhost:${{ job.services.openbao.ports[8210] }} + DEX_OPENBAO_TOKEN: root-token + DEX_KUBERNETES_CONFIG_PATH: ~/.kube/config + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version: "1.25" + + - name: Download golangci-lint + run: make bin/golangci-lint + - name: Lint run: make lint - # Ensure proto generation doesn't depend on external packages. - - name: Verify proto - run: make verify-proto + artifacts: + name: Artifacts + uses: ./.github/workflows/artifacts.yaml + with: + publish: ${{ github.event_name == 'push' }} + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + permissions: + attestations: write + contents: read + packages: write + id-token: write + security-events: write + + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dependency Review + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml deleted file mode 100644 index 926f8be539..0000000000 --- a/.github/workflows/codeql-analysis.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master, v1 ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '28 10 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml deleted file mode 100644 index f841d55640..0000000000 --- a/.github/workflows/docker.yaml +++ /dev/null @@ -1,111 +0,0 @@ -name: Docker - -on: - # push: - # branches: - # - master - # tags: - # - v[0-9]+.[0-9]+.[0-9]+ - pull_request: - -jobs: - docker: - name: Docker - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Calculate Docker image tags - id: tags - env: - DOCKER_IMAGES: "ghcr.io/dexidp/dex dexidp/dex" - run: | - case $GITHUB_REF in - refs/tags/*) VERSION=${GITHUB_REF#refs/tags/};; - refs/heads/*) VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g');; - refs/pull/*) VERSION=pr-${{ github.event.number }};; - *) VERSION=sha-${GITHUB_SHA::8};; - esac - - TAGS=() - for image in $DOCKER_IMAGES; do - TAGS+=("${image}:${VERSION}") - - if [[ "${{ github.event.repository.default_branch }}" == "$VERSION" ]]; then - TAGS+=("${image}:latest") - fi - done - - echo ::set-output name=version::${VERSION} - echo ::set-output name=tags::$(IFS=,; echo "${TAGS[*]}") - echo ::set-output name=commit_hash::${GITHUB_SHA::8} - echo ::set-output name=build_date::$(git show -s --format=%cI) - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - with: - platforms: all - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - 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 GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} - if: github.event_name == 'push' - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - if: github.event_name == 'push' - - - name: Build and push - uses: docker/build-push-action@v3 - with: - context: . - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le - # cache-from: type=gha - # cache-to: type=gha,mode=max - push: ${{ github.event_name == 'push' }} - tags: ${{ steps.tags.outputs.tags }} - build-args: | - VERSION=${{ steps.tags.outputs.version }} - COMMIT_HASH=${{ steps.tags.outputs.commit_hash }} - BUILD_DATE=${{ steps.tags.outputs.build_date }} - 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.tags.outputs.version }} - org.opencontainers.image.created=${{ steps.tags.outputs.build_date }} - org.opencontainers.image.revision=${{ github.sha }} - org.opencontainers.image.licenses=${{ github.event.repository.license.spdx_id }} - org.opencontainers.image.documentation=https://dexidp.io/docs/ - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.7.1 - with: - image-ref: "ghcr.io/dexidp/dex:${{ steps.tags.outputs.version }}" - format: "template" - template: "@/contrib/sarif.tpl" - output: "trivy-results.sarif" - if: github.event_name == 'push' - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: "trivy-results.sarif" - if: github.event_name == 'push' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000000..dbf397cbbe --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,24 @@ +name: Release + +on: + push: + tags: [ "v[0-9]+.[0-9]+.[0-9]+" ] + +permissions: + contents: read + +jobs: + artifacts: + name: Artifacts + uses: ./.github/workflows/artifacts.yaml + with: + publish: true + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + permissions: + attestations: write + contents: read + packages: write + id-token: write + security-events: write diff --git a/.github/workflows/trivydb-cache.yaml b/.github/workflows/trivydb-cache.yaml new file mode 100644 index 0000000000..b64bd38e2d --- /dev/null +++ b/.github/workflows/trivydb-cache.yaml @@ -0,0 +1,42 @@ +# Note: This workflow only updates the cache. You should create a separate workflow for your actual Trivy scans. +# In your scan workflow, set TRIVY_SKIP_DB_UPDATE=true and TRIVY_SKIP_JAVA_DB_UPDATE=true. +name: Update Trivy Cache + +on: + schedule: + - cron: '0 0 * * *' # Run daily at midnight UTC + workflow_dispatch: # Allow manual triggering + +permissions: + contents: read + +jobs: + update-trivy-db: + runs-on: ubuntu-latest + steps: + - name: Setup oras + uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1.2.4 + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Download and extract the vulnerability DB + run: | + mkdir -p $GITHUB_WORKSPACE/.cache/trivy/db + oras pull ghcr.io/aquasecurity/trivy-db:2 + tar -xzf db.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/db + rm db.tar.gz + + - name: Download and extract the Java DB + run: | + mkdir -p $GITHUB_WORKSPACE/.cache/trivy/java-db + oras pull ghcr.io/aquasecurity/trivy-java-db:1 + tar -xzf javadb.tar.gz -C $GITHUB_WORKSPACE/.cache/trivy/java-db + rm javadb.tar.gz + + - name: Cache DBs + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: ${{ github.workspace }}/.cache/trivy + key: cache-trivy-${{ steps.date.outputs.date }} diff --git a/.gitignore b/.gitignore index 66dc41ccfe..7e5ae46ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.devenv/ /.direnv/ /.idea/ /bin/ @@ -5,3 +6,4 @@ /docker-compose.override.yaml /var/ /vendor/ +*.db diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000000..9fa3141874 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,124 @@ +version: "2" + +run: + timeout: 5m + +linters: + disable: + - staticcheck + - errcheck + enable: + - depguard + - dogsled + - exhaustive + - gochecknoinits + # - gocritic + - goprintffuncname + - govet + - ineffassign + - misspell + - nakedret + - nolintlint + - prealloc + # - revive + # - sqlclosecheck + # - staticcheck + - unconvert + - unused + - whitespace + + # Disable temporarily until everything works with Go 1.20 + # - bodyclose + # - rowserrcheck + # - tparallel + # - unparam + + # Disable temporarily until the following issue is resolved: https://github.com/golangci/golangci-lint/issues/3086 + # - sqlclosecheck + + # TODO: fix linter errors before enabling + # - exhaustivestruct + # - gochecknoglobals + # - errorlint + # - gocognit + # - godot + # - nlreturn + # - noctx + # - revive + # - wrapcheck + + # TODO: fix linter errors before enabling (from original config) + # - dupl + # - errcheck + # - goconst + # - gocyclo + # - gosec + # - lll + # - scopelint + + # unused + # - goheader + # - gomodguard + + # don't enable: + # - asciicheck + # - funlen + # - godox + # - goerr113 + # - gomnd + # - interfacer + # - maligned + # - nestif + # - testpackage + # - wsl + + exclusions: + rules: + - linters: + - errcheck + - noctx + path: _test.go + presets: + - comments + - std-error-handling + + settings: + misspell: + locale: US + nolintlint: + allow-unused: false # report any unused nolint directives + require-specific: false # don't require nolint directives to be specific about which linter is being skipped + gocritic: + # Enable multiple checks by tags. See "Tags" section in https://github.com/go-critic/go-critic#usage. + enabled-tags: + - diagnostic + - experimental + - opinionated + - style + disabled-checks: + - importShadow + - unnamedResult + depguard: + rules: + deprecated: + deny: + - pkg: "io/ioutil" + desc: "The 'io/ioutil' package is deprecated. Use corresponding 'os' or 'io' functions instead." + +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + # - golines + + settings: + gci: + sections: + - standard + - default + - localmodule +# issues: +# exclude-dirs: +# - storage/ent/db # generated ent code diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index cfb64a75bf..0000000000 --- a/.golangci.yml +++ /dev/null @@ -1,90 +0,0 @@ -run: - timeout: 4m - -linters-settings: - depguard: - list-type: blacklist - include-go-root: true - packages: - - io/ioutil - packages-with-error-message: - - io/ioutil: "The 'io/ioutil' package is deprecated. Use corresponding 'os' or 'io' functions instead." - gci: - local-prefixes: github.com/dexidp/dex - goimports: - local-prefixes: github.com/dexidp/dex - - -linters: - disable-all: true - enable: - - bodyclose - - deadcode - - depguard - - dogsled - - exhaustive - - exportloopref - - gci - - gochecknoinits - - gocritic - - gofmt - - gofumpt - - goimports - - goprintffuncname - - gosimple - - govet - - ineffassign - - misspell - - nakedret - - nolintlint - - prealloc - - revive - - rowserrcheck - - sqlclosecheck - - staticcheck - - structcheck - - stylecheck - - tparallel - - unconvert - - unparam - - unused - - varcheck - - whitespace - - # Disable temporarily until everything works with Go 1.18 - # - typecheck - - # TODO: fix linter errors before enabling - # - exhaustivestruct - # - gochecknoglobals - # - errorlint - # - gocognit - # - godot - # - nlreturn - # - noctx - # - wrapcheck - - # TODO: fix linter errors before enabling (from original config) - # - dupl - # - errcheck - # - goconst - # - gocyclo - # - gosec - # - lll - # - scopelint - - # unused - # - goheader - # - gomodguard - - # don't enable: - # - asciicheck - # - funlen - # - godox - # - goerr113 - # - gomnd - # - interfacer - # - maligned - # - nestif - # - testpackage - # - wsl diff --git a/ADOPTERS.md b/ADOPTERS.md index 50f9ba988d..88a835cbdd 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,15 +1,26 @@ # Adopters -This is a list of production adopters of Dex (in alphabetical order): +This is a list of production adopters of Dex (in alphabetical order). + +# Companies - [Aspect](https://www.aspect.com/) uses Dex for authenticating users across their Kubernetes infrastructure (using Kubernetes OIDC support). - [Banzai Cloud](https://banzaicloud.com) is using Dex for authenticating to its Pipeline control plane and also to authenticate users against provisioned Kubernetes clusters (via Kubernetes OIDC support). -- [Chef](https://chef.io) uses Dex for authenticating users in [Chef Automate](https://automate.chef.io/). The code is Open Source, available at [`github.com/chef/automate`](https://github.com/chef/automate). -- [Elastisys](https://elastisys.com) uses Dex for authentication in their [Compliant Kubernetes](https://compliantkubernetes.io) distribution, including SSO to the custom dashboard, Grafana, Kibana, and Harbor. +- [Ericsson](https://www.ericsson.com) is using Dex to authenticate access to Kubernetes API server in [Cloud Container Distribution](https://www.ericsson.com/en/portfolio/cloud-software-and-services/cloud-core/cloud-infrastructure/nfvi/cloud-container-distribution). - [Flant](https://flant.com) uses Dex for providing access to core components of [Managed Kubernetes as a Service](https://flant.com/services/managed-kubernetes-as-a-service), integration with various authentication providers, plugging custom applications. - [JuliaBox](https://juliabox.com/) is leveraging federated OIDC provided by Dex for authenticating users to their compute infrastructure based on Kubernetes. +- [Pusher](https://pusher.com) uses Dex for authenticating users across their Kubernetes infrastructure (using Kubernetes OIDC support) in conjunction with the [OAuth2 Proxy](https://github.com/pusher/oauth2_proxy) for protecting web UIs. + +# Projects + +- [Argo CD](https://argoproj.github.io/cd) integrates Dex to provide convenient Single Sign On capabilities to its web UI and CLI +- [Chef](https://chef.io) uses Dex for authenticating users in [Chef Automate](https://automate.chef.io/). The code is Open Source, available at [`github.com/chef/automate`](https://github.com/chef/automate). +- [Elastisys](https://elastisys.com) uses Dex for authentication in [Welkin, The Application Platform for Software Critical to Society](https://elastisys.io/welkin/), including SSO to Grafana, OpenSearch, and Harbor. - [Kasten](https://www.kasten.io) is using Dex for authenticating access to the dashboard of [K10](https://www.kasten.io/product/), a Kubernetes-native platform for backup, disaster recovery and mobility of Kubernetes applications. K10 is widely used by a variety of customers including large enterprises, financial services, design firms, and IT companies. +- [Kubeflow](https://www.kubeflow.org/) [uses](https://github.com/kubeflow/manifests#dex) Dex as one of its components in the Kubeflow Platform for external OIDC authentication. - [Kyma](https://kyma-project.io) is using Dex to authenticate access to Kubernetes API server (even for managed Kubernetes like Google Kubernetes Engine or Azure Kubernetes Service) and for protecting web UI of [Kyma Console](https://github.com/kyma-project/console) and other UIs integrated in Kyma ([Grafana](https://github.com/grafana/grafana), [Loki](https://github.com/grafana/loki), and [Jaeger](https://github.com/jaegertracing/jaeger)). Kyma is an open-source project ([`github.com/kyma-project`](https://github.com/kyma-project/kyma)) designed natively on Kubernetes, that allows you to extend and customize your applications in a quick and modern way, using serverless computing or microservice architecture. -- [Pusher](https://pusher.com) uses Dex for authenticating users across their Kubernetes infrastructure (using Kubernetes OIDC support) in conjunction with the [OAuth2 Proxy](https://github.com/pusher/oauth2_proxy) for protecting web UIs. +- [LitmusChaos](https://litmuschaos.io/) uses Dex to [implement](https://docs.litmuschaos.io/docs/user-guides/chaoscenter-oauth-dex-installation#deploy-dex-oidc-provider) OAuth2 login support in ChaosCenter, its centralized chaos management tool. +- [LLMariner](https://llmariner.ai/) uses Dex for [user management](https://llmariner.ai/docs/features/user_management/). - [Pydio](https://pydio.com/) Pydio Cells is an open source sync & share platform written in Go. Cells is using Dex as an OIDC service for authentication and authorizations. Check out [Pydio Cells repository](https://github.com/pydio/cells) for more information and/or to contribute. - [sigstore](https://sigstore.dev) uses Dex for authentication in their public Fulcio instance, which is a certificate authority for code signing certificates bound to OIDC-based identities. +- [Terrakube](https://docs.terrakube.io/) relies on Dex for [user authentication](https://docs.terrakube.io/getting-started/deployment/user-authentication-dex). Its Helm chart uses Dex as a dependency. diff --git a/Dockerfile b/Dockerfile index 3462ae52ac..69f25eb70a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,48 +1,65 @@ ARG BASE_IMAGE=alpine -FROM golang:1.19.1-alpine3.16 AS builder +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx -WORKDIR /usr/local/src/dex +FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine3.22@sha256:07e91d24f6330432729082bb580983181809e0a48f0f38ecde26868d4568c6ac AS builder -RUN apk add --no-cache --update alpine-sdk ca-certificates openssl +COPY --from=xx / / -ARG TARGETOS -ARG TARGETARCH -ARG TARGETVARIANT="" +RUN apk add --update alpine-sdk ca-certificates openssl clang lld + +ARG TARGETPLATFORM -ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT} +RUN xx-apk --update add musl-dev gcc + +# lld has issues building static binaries for ppc so prefer ld for it +RUN [ "$(xx-info arch)" != "ppc64le" ] || XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple + +RUN xx-go --wrap + +WORKDIR /usr/local/src/dex ARG GOPROXY +ENV CGO_ENABLED=1 + COPY go.mod go.sum ./ COPY api/v2/go.mod api/v2/go.sum ./api/v2/ RUN go mod download COPY . . +# Propagate Dex version from build args to the build environment +ARG VERSION RUN make release-binary -FROM alpine:3.16.2 AS stager +RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint + +FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 AS stager RUN mkdir -p /var/dex RUN mkdir -p /etc/dex COPY config.docker.yaml /etc/dex/ -FROM alpine:3.16.2 AS gomplate +FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 AS gomplate ARG TARGETOS ARG TARGETARCH ARG TARGETVARIANT -ENV GOMPLATE_VERSION=v3.11.2 +ENV GOMPLATE_VERSION=v5.0.0 RUN wget -O /usr/local/bin/gomplate \ "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \ && chmod +x /usr/local/bin/gomplate # For Dependabot to detect base image versions -FROM alpine:3.16.2 AS alpine -FROM gcr.io/distroless/static:latest AS distroless +FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 AS alpine + +FROM alpine AS user-setup +RUN addgroup -g 1001 -S dex && adduser -u 1001 -S -G dex -D -H -s /sbin/nologin dex + +FROM gcr.io/distroless/static-debian13:nonroot@sha256:e3f945647ffb95b5839c07038d64f9811adf17308b9121d8a2b87b6a22a80a39 AS distroless FROM $BASE_IMAGE @@ -53,6 +70,10 @@ FROM $BASE_IMAGE # See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations. COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +# Ensure the dex user/group exist before setting ownership or switching to them. +COPY --from=user-setup /etc/passwd /etc/passwd +COPY --from=user-setup /etc/group /etc/group + COPY --from=stager --chown=1001:1001 /var/dex /var/dex COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex @@ -66,7 +87,7 @@ COPY --from=builder /usr/local/src/dex/web /srv/dex/web COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate -USER 1001:1001 +USER dex:dex ENTRYPOINT ["/usr/local/bin/docker-entrypoint"] CMD ["dex", "serve", "/etc/dex/config.docker.yaml"] diff --git a/Makefile b/Makefile index 9c55ad2b11..2722be7d8d 100644 --- a/Makefile +++ b/Makefile @@ -1,139 +1,85 @@ -OS = $(shell uname | tr A-Z a-z) - export PATH := $(abspath bin/protoc/bin/):$(abspath bin/):${PATH} -PROJ=dex -ORG_PATH=github.com/dexidp -REPO_PATH=$(ORG_PATH)/$(PROJ) - -VERSION ?= $(shell ./scripts/git-version) +OS = $(shell uname | tr A-Z a-z) -DOCKER_REPO=quay.io/dexidp/dex -DOCKER_IMAGE=$(DOCKER_REPO):$(VERSION) +user=$(shell id -u -n) +group=$(shell id -g -n) $( shell mkdir -p bin ) -user=$(shell id -u -n) -group=$(shell id -g -n) +PROJ = dex +ORG_PATH = github.com/dexidp +REPO_PATH = $(ORG_PATH)/$(PROJ) +VERSION ?= $(shell ./scripts/git-version) -export GOBIN=$(PWD)/bin +export GOBIN=$(PWD)/bin LD_FLAGS="-w -X main.version=$(VERSION)" # Dependency versions +GOLANGCI_VERSION = 2.4.0 +GOTESTSUM_VERSION ?= 1.12.0 -KIND_NODE_IMAGE = "kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729" -KIND_TMP_DIR = "$(PWD)/bin/test/dex-kind-kubeconfig" +PROTOC_VERSION = 29.3 +PROTOC_GEN_GO_VERSION = 1.36.5 +PROTOC_GEN_GO_GRPC_VERSION = 1.5.1 -.PHONY: generate -generate: - @go generate $(REPO_PATH)/storage/ent/ +KIND_VERSION = 0.22.0 +KIND_NODE_IMAGE = "kindest/node:v1.25.3@sha256:cd248d1438192f7814fbca8fede13cfe5b9918746dfa12583976158a834fd5c5" +KIND_TMP_DIR = "$(PWD)/bin/test/dex-kind-kubeconfig" -build: generate bin/dex -bin/dex: - @mkdir -p bin/ - @go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex +##@ Build -examples: bin/grpc-client bin/example-app +build: bin/dex ## Build Dex binaries. -bin/grpc-client: - @mkdir -p bin/ - @cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/grpc-client +examples: bin/grpc-client bin/example-app ## Build example app. -bin/example-app: - @mkdir -p bin/ - @cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/example-app +.PHONY: update-gomplate +update-gomplate: ## Check and update gomplate version in Dockerfile. + @./scripts/update-gomplate .PHONY: release-binary release-binary: LD_FLAGS = "-w -X main.version=$(VERSION) -extldflags \"-static\"" -release-binary: generate +release-binary: ## Build release binaries (used to build a final container image). @go build -o /go/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex @go build -o /go/bin/docker-entrypoint -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/docker-entrypoint -docker-compose.override.yaml: - cp docker-compose.override.yaml.dist docker-compose.override.yaml - -.PHONY: up -up: docker-compose.override.yaml ## Launch the development environment - @ if [ docker-compose.override.yaml -ot docker-compose.override.yaml.dist ]; then diff -u docker-compose.override.yaml docker-compose.override.yaml.dist || (echo "!!! The distributed docker-compose.override.yaml example changed. Please update your file accordingly (or at least touch it). !!!" && false); fi - docker-compose up -d - -.PHONY: down -down: clear ## Destroy the development environment - docker-compose down --volumes --remove-orphans --rmi local - -test: - @go test -v ./... - -testrace: - @go test -v --race ./... - -.PHONY: kind-up kind-down kind-tests -kind-up: - @mkdir -p bin/test - @kind create cluster --image ${KIND_NODE_IMAGE} --kubeconfig ${KIND_TMP_DIR} - -kind-down: - @kind delete cluster - rm ${KIND_TMP_DIR} - -kind-tests: export DEX_KUBERNETES_CONFIG_PATH=${KIND_TMP_DIR} -kind-tests: testall - -.PHONY: lint lint-fix -lint: ## Run linter - golangci-lint run - -.PHONY: fix -fix: ## Fix lint violations - golangci-lint run --fix +bin/dex: + @mkdir -p bin/ + @go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex -.PHONY: docker-image -docker-image: - @sudo docker build -t $(DOCKER_IMAGE) . +bin/grpc-client: + @mkdir -p bin/ + @cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/grpc-client -.PHONY: verify-proto -verify-proto: proto - @./scripts/git-diff +bin/example-app: + @mkdir -p bin/ + @cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/example-app -clean: - @rm -rf bin/ -testall: testrace +##@ Generate -FORCE: +.PHONY: generate +generate: generate-proto generate-proto-internal generate-ent go-mod-tidy ## Run all generators. -.PHONY: test testrace testall +.PHONY: generate-ent +generate-ent: ## Generate code for database ORM. + @go generate $(REPO_PATH)/storage/ent/ -.PHONY: proto -proto: +.PHONY: generate-proto +generate-proto: ## Generate the Dex client's protobuf code. @protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. api/v2/*.proto @protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. api/*.proto - #@cp api/v2/*.proto api/ -.PHONY: proto-internal -proto-internal: +.PHONY: generate-proto-internal +generate-proto-internal: ## Generate protobuf code for token encoding. @protoc --go_out=paths=source_relative:. server/internal/*.proto -# Dependency versions -GOLANGCI_VERSION = 1.46.0 -GOTESTSUM_VERSION ?= 1.7.0 -PROTOC_VERSION = 3.15.6 -PROTOC_GEN_GO_VERSION = 1.26.0 -PROTOC_GEN_GO_GRPC_VERSION = 1.1.0 -KIND_VERSION = 0.11.1 - -deps: bin/gotestsum bin/golangci-lint bin/protoc bin/protoc-gen-go bin/protoc-gen-go-grpc bin/kind - -bin/gotestsum: - @mkdir -p bin - curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_$(shell uname | tr A-Z a-z)_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum - @chmod +x ./bin/gotestsum - -bin/golangci-lint: - @mkdir -p bin - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION} +go-mod-tidy: ## Run go mod tidy for all targets. + @go mod tidy + @cd examples/ && go mod tidy + @cd api/v2/ && go mod tidy bin/protoc: @mkdir -p bin/protoc @@ -156,7 +102,128 @@ bin/protoc-gen-go-grpc: curl -L https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTOC_GEN_GO_GRPC_VERSION}/protoc-gen-go-grpc.v${PROTOC_GEN_GO_GRPC_VERSION}.$(shell uname | tr A-Z a-z).amd64.tar.gz | tar -zOxf - ./protoc-gen-go-grpc > ./bin/protoc-gen-go-grpc @chmod +x ./bin/protoc-gen-go-grpc +##@ Verify + +verify: generate ## Verify that all the code was generated and committed to repository. + @git diff --exit-code + +.PHONY: verify-proto +verify-proto: generate-proto ## Verify that the Dex client's protobuf code was generated. + @git diff --exit-code + +.PHONY: verify-proto +verify-proto-internal: generate-proto-internal ## Verify internal protobuf code for token encoding was generated. + @git diff --exit-code + +.PHONY: verify-ent +verify-ent: generate-ent ## Verify code for database ORM was generated. + @git diff --exit-code + +.PHONY: verify-go-mod +verify-go-mod: go-mod-tidy ## Check that go.mod and go.sum formatted according to the changes. + @git diff --exit-code + +##@ Test and Lint + +deps: bin/gotestsum bin/golangci-lint bin/protoc bin/protoc-gen-go bin/protoc-gen-go-grpc bin/kind ## Install dev dependencies. + +# Detect if we're running in GitHub Actions +ifdef GITHUB_ACTIONS +GOTESTSUM_FORMAT = github-actions +else +GOTESTSUM_FORMAT = testname +GOTESTSUM_FORMAT_ICONS = hivis +endif + +.PHONY: test testrace testall +test: bin/gotestsum ## Test go code. +ifdef GOTESTSUM_FORMAT_ICONS + @gotestsum --format $(GOTESTSUM_FORMAT) --format-icons $(GOTESTSUM_FORMAT_ICONS) -- -v ./... +else + @gotestsum --format $(GOTESTSUM_FORMAT) -- -v ./... +endif + +testrace: bin/gotestsum ## Test go code and check for possible race conditions. +ifdef GOTESTSUM_FORMAT_ICONS + @gotestsum --format $(GOTESTSUM_FORMAT) --format-icons $(GOTESTSUM_FORMAT_ICONS) -- -v --race ./... +else + @gotestsum --format $(GOTESTSUM_FORMAT) -- -v --race ./... +endif + +testall: testrace ## Run all tests for go code. + +.PHONY: lint +lint: ## Run linter. + @golangci-lint version + @golangci-lint run + +.PHONY: fix +fix: ## Fix lint violations. + @golangci-lint version + @golangci-lint fmt + +docker-compose.override.yaml: + cp docker-compose.override.yaml.dist docker-compose.override.yaml + +.PHONY: up +up: docker-compose.override.yaml ## Launch the development environment. + @ if [ docker-compose.override.yaml -ot docker-compose.override.yaml.dist ]; then diff -u docker-compose.override.yaml docker-compose.override.yaml.dist || (echo "!!! The distributed docker-compose.override.yaml example changed. Please update your file accordingly (or at least touch it). !!!" && false); fi + docker-compose up -d + +.PHONY: down +down: clear ## Destroy the development environment. + docker-compose down --volumes --remove-orphans --rmi local + +.PHONY: kind-up kind-down kind-tests +kind-up: ## Create a kind cluster. + @mkdir -p bin/test + @kind create cluster --image ${KIND_NODE_IMAGE} --kubeconfig ${KIND_TMP_DIR} --name dex-tests + +kind-tests: export DEX_KUBERNETES_CONFIG_PATH=${KIND_TMP_DIR} +kind-tests: testall ## Run test on kind cluster (kind cluster must be created). + +kind-down: ## Delete the kind cluster. + @kind delete cluster --name dex-tests + rm ${KIND_TMP_DIR} + +bin/golangci-lint: + @mkdir -p bin + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION} + +bin/gotestsum: + @mkdir -p bin + curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_$(shell uname | tr A-Z a-z)_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum + @chmod +x ./bin/gotestsum + bin/kind: @mkdir -p bin curl -L https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-$(shell uname | tr A-Z a-z)-amd64 > ./bin/kind @chmod +x ./bin/kind + +##@ Clean +clean: ## Delete all builds and downloaded dependencies. + @rm -rf bin/ + + +FORMATTING_BEGIN_YELLOW = \033[0;33m +FORMATTING_BEGIN_BLUE = \033[36m +FORMATTING_END = \033[0m + +.PHONY: help +help: + @printf -- "${FORMATTING_BEGIN_BLUE}%s${FORMATTING_END}\n" \ + "" \ + " ___ " \ + " / _ \_____ __ " \ + " / // / -_) \ / " \ + " /____/\__/_\_\ " \ + "" \ + "-----------------------" \ + "" + @awk 'BEGIN {\ + FS = ":.*##"; \ + printf "Usage: ${FORMATTING_BEGIN_BLUE}OPTION${FORMATTING_END}= make ${FORMATTING_BEGIN_YELLOW}${FORMATTING_END}\n"\ + } \ + /^[a-zA-Z0-9_-]+:.*?##/ { printf " ${FORMATTING_BEGIN_BLUE}%-46s${FORMATTING_END} %s\n", $$1, $$2 } \ + /^.?.?##~/ { printf " %-46s${FORMATTING_BEGIN_YELLOW}%-46s${FORMATTING_END}\n", "", substr($$1, 6) } \ + /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 271376d65e..dac886ee4a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # dex - A federated OpenID Connect provider -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/dexidp/dex/CI?style=flat-square) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dexidp/dex/ci.yaml?style=flat-square&branch=master) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/dexidp/dex/badge?style=flat-square)](https://api.securityscorecards.dev/projects/github.com/dexidp/dex) [![Go Report Card](https://goreportcard.com/badge/github.com/dexidp/dex?style=flat-square)](https://goreportcard.com/report/github.com/dexidp/dex) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&style=flat-square)](https://gitpod.io/#https://github.com/dexidp/dex) @@ -12,7 +13,7 @@ Dex acts as a portal to other identity providers through ["connectors."](#connec ## ID Tokens -ID Tokens are an OAuth2 extension introduced by OpenID Connect and dex's primary feature. ID Tokens are [JSON Web Tokens][jwt-io] (JWTs) signed by dex and returned as part of the OAuth2 response that attest to the end user's identity. An example JWT might look like: +ID Tokens are an OAuth2 extension introduced by OpenID Connect and dex's primary feature. ID Tokens are [JSON Web Tokens][jwt-io] (JWTs) signed by dex and returned as part of the OAuth2 response that attests to the end user's identity. An example JWT might look like: ``` eyJhbGciOiJSUzI1NiIsImtpZCI6IjlkNDQ3NDFmNzczYjkzOGNmNjVkZDMyNjY4NWI4NjE4MGMzMjRkOTkifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2djeU16UXlOelE1RWdabmFYUm9kV0kiLCJhdWQiOiJleGFtcGxlLWFwcCIsImV4cCI6MTQ5Mjg4MjA0MiwiaWF0IjoxNDkyNzk1NjQyLCJhdF9oYXNoIjoiYmk5NmdPWFpTaHZsV1l0YWw5RXFpdyIsImVtYWlsIjoiZXJpYy5jaGlhbmdAY29yZW9zLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJncm91cHMiOlsiYWRtaW5zIiwiZGV2ZWxvcGVycyJdLCJuYW1lIjoiRXJpYyBDaGlhbmcifQ.OhROPq_0eP-zsQRjg87KZ4wGkjiQGnTi5QuG877AdJDb3R2ZCOk2Vkf5SdP8cPyb3VMqL32G4hLDayniiv8f1_ZXAde0sKrayfQ10XAXFgZl_P1yilkLdknxn6nbhDRVllpWcB12ki9vmAxklAr0B1C4kr5nI3-BZLrFcUR5sQbxwJj4oW1OuG6jJCNGHXGNTBTNEaM28eD-9nhfBeuBTzzO7BKwPsojjj4C9ogU4JQhGvm_l4yfVi0boSx8c0FX3JsiB0yLa1ZdJVWVl9m90XmbWRSD85pNDQHcWZP9hR6CMgbvGkZsgjG32qeRwUL_eNkNowSBNWLrGNPoON1gMg @@ -49,8 +50,8 @@ For details on how to request or validate an ID Token, see [_"Writing apps that Dex runs natively on top of any Kubernetes cluster using Custom Resource Definitions and can drive API server authentication through the OpenID Connect plugin. Clients, such as the [`kubernetes-dashboard`](https://github.com/kubernetes/dashboard) and `kubectl`, can act on behalf of users who can login to the cluster through any identity provider dex supports. -* More docs for running dex as a Kubernetes authenticator can be found [here](https://dexidp.io/docs/kubernetes/). -* You can find more about companies and projects, which uses dex, [here](./ADOPTERS.md). +* More docs for running dex as a Kubernetes authenticator can be found [here](https://dexidp.io/docs/guides/kubernetes/). +* You can find more about companies and projects which use dex, [here](./ADOPTERS.md). ## Connectors @@ -77,8 +78,8 @@ Dex implements the following connectors: | [Microsoft](https://dexidp.io/docs/connectors/microsoft/) | yes | yes | no | beta | | | [AuthProxy](https://dexidp.io/docs/connectors/authproxy/) | no | yes | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. | | [Bitbucket Cloud](https://dexidp.io/docs/connectors/bitbucketcloud/) | yes | yes | no | alpha | | -| [OpenShift](https://dexidp.io/docs/connectors/openshift/) | no | yes | no | alpha | | -| [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | +| [OpenShift](https://dexidp.io/docs/connectors/openshift/) | yes | yes | no | alpha | | +| [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassian-crowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | beta | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | @@ -95,7 +96,7 @@ All changes or deprecations of connector features will be announced in the [rele * [Getting started](https://dexidp.io/docs/getting-started/) * [Intro to OpenID Connect](https://dexidp.io/docs/openid-connect/) * [Writing apps that use dex][using-dex] -* [What's new in v2](https://dexidp.io/docs/v2/) +* [What's new in v2](https://dexidp.io/docs/archive/v2/) * [Custom scopes, claims, and client features](https://dexidp.io/docs/custom-scopes-claims-clients/) * [Storage options](https://dexidp.io/docs/storage/) * [gRPC API](https://dexidp.io/docs/api/) @@ -120,7 +121,7 @@ Please see our [security policy](.github/SECURITY.md) for details about reportin [scopes]: https://dexidp.io/docs/custom-scopes-claims-clients/#scopes [using-dex]: https://dexidp.io/docs/using-dex/ [jwt-io]: https://jwt.io/ -[kubernetes]: http://kubernetes.io/docs/admin/authentication/#openid-connect-tokens +[kubernetes]: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens [aws-sts]: https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html [go-oidc]: https://github.com/coreos/go-oidc [issue-1065]: https://github.com/dexidp/dex/issues/1065 @@ -138,6 +139,8 @@ For the best developer experience, install [Nix](https://builtwithnix.org/) and Alternatively, install Go and Docker manually or using a package manager. Install the rest of the dependencies by running `make deps`. +For release process, please read the [release documentation](https://dexidp.io/docs/development/releases/). + ## License The project is licensed under the [Apache License, Version 2.0](LICENSE). diff --git a/api/api.pb.go b/api/api.pb.go index 6d1c2ca82e..23c6141071 100644 --- a/api/api.pb.go +++ b/api/api.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 -// protoc v3.15.6 +// protoc-gen-go v1.36.5 +// protoc v5.29.3 // source: api/api.proto package api @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,26 +23,24 @@ const ( // Client represents an OAuth2 client. type Client struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - RedirectUris []string `protobuf:"bytes,3,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` - TrustedPeers []string `protobuf:"bytes,4,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` - Public bool `protobuf:"varint,5,opt,name=public,proto3" json:"public,omitempty"` - Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` - LogoUrl string `protobuf:"bytes,7,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + RedirectUris []string `protobuf:"bytes,3,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,4,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Public bool `protobuf:"varint,5,opt,name=public,proto3" json:"public,omitempty"` + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,7,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + AllowedConnectors []string `protobuf:"bytes,8,rep,name=allowed_connectors,json=allowedConnectors,proto3" json:"allowed_connectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Client) Reset() { *x = Client{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Client) String() string { @@ -52,7 +51,7 @@ func (*Client) ProtoMessage() {} func (x *Client) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -116,22 +115,26 @@ func (x *Client) GetLogoUrl() string { return "" } +func (x *Client) GetAllowedConnectors() []string { + if x != nil { + return x.AllowedConnectors + } + return nil +} + // CreateClientReq is a request to make a client. type CreateClientReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"` unknownFields protoimpl.UnknownFields - - Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateClientReq) Reset() { *x = CreateClientReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateClientReq) String() string { @@ -142,7 +145,7 @@ func (*CreateClientReq) ProtoMessage() {} func (x *CreateClientReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -166,21 +169,18 @@ func (x *CreateClientReq) GetClient() *Client { // CreateClientResp returns the response from creating a client. type CreateClientResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` + Client *Client `protobuf:"bytes,2,opt,name=client,proto3" json:"client,omitempty"` unknownFields protoimpl.UnknownFields - - AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` - Client *Client `protobuf:"bytes,2,opt,name=client,proto3" json:"client,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateClientResp) Reset() { *x = CreateClientResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateClientResp) String() string { @@ -191,7 +191,7 @@ func (*CreateClientResp) ProtoMessage() {} func (x *CreateClientResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -222,21 +222,18 @@ func (x *CreateClientResp) GetClient() *Client { // DeleteClientReq is a request to delete a client. type DeleteClientReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The ID of the client. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteClientReq) Reset() { *x = DeleteClientReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteClientReq) String() string { @@ -247,7 +244,7 @@ func (*DeleteClientReq) ProtoMessage() {} func (x *DeleteClientReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -271,20 +268,17 @@ func (x *DeleteClientReq) GetId() string { // DeleteClientResp determines if the client is deleted successfully. type DeleteClientResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteClientResp) Reset() { *x = DeleteClientResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteClientResp) String() string { @@ -295,7 +289,7 @@ func (*DeleteClientResp) ProtoMessage() {} func (x *DeleteClientResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -319,24 +313,22 @@ func (x *DeleteClientResp) GetNotFound() bool { // UpdateClientReq is a request to update an existing client. type UpdateClientReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` - TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` - LogoUrl string `protobuf:"bytes,5,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,5,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + AllowedConnectors []string `protobuf:"bytes,6,rep,name=allowed_connectors,json=allowedConnectors,proto3" json:"allowed_connectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateClientReq) Reset() { *x = UpdateClientReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdateClientReq) String() string { @@ -347,7 +339,7 @@ func (*UpdateClientReq) ProtoMessage() {} func (x *UpdateClientReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -397,22 +389,26 @@ func (x *UpdateClientReq) GetLogoUrl() string { return "" } +func (x *UpdateClientReq) GetAllowedConnectors() []string { + if x != nil { + return x.AllowedConnectors + } + return nil +} + // UpdateClientResp returns the response from updating a client. type UpdateClientResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *UpdateClientResp) Reset() { *x = UpdateClientResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdateClientResp) String() string { @@ -423,7 +419,7 @@ func (*UpdateClientResp) ProtoMessage() {} func (x *UpdateClientResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -447,24 +443,21 @@ func (x *UpdateClientResp) GetNotFound() bool { // Password is an email for password mapping managed by the storage. type Password struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Currently we do not accept plain text passwords. Could be an option in the future. - Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` - Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` - UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Password) Reset() { *x = Password{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Password) String() string { @@ -475,7 +468,7 @@ func (*Password) ProtoMessage() {} func (x *Password) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -520,20 +513,17 @@ func (x *Password) GetUserId() string { // CreatePasswordReq is a request to make a password. type CreatePasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Password *Password `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields - - Password *Password `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreatePasswordReq) Reset() { *x = CreatePasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreatePasswordReq) String() string { @@ -544,7 +534,7 @@ func (*CreatePasswordReq) ProtoMessage() {} func (x *CreatePasswordReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -568,20 +558,17 @@ func (x *CreatePasswordReq) GetPassword() *Password { // CreatePasswordResp returns the response from creating a password. type CreatePasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` unknownFields protoimpl.UnknownFields - - AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreatePasswordResp) Reset() { *x = CreatePasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreatePasswordResp) String() string { @@ -592,7 +579,7 @@ func (*CreatePasswordResp) ProtoMessage() {} func (x *CreatePasswordResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -616,23 +603,20 @@ func (x *CreatePasswordResp) GetAlreadyExists() bool { // UpdatePasswordReq is a request to modify an existing password. type UpdatePasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The email used to lookup the password. This field cannot be modified - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - NewHash []byte `protobuf:"bytes,2,opt,name=new_hash,json=newHash,proto3" json:"new_hash,omitempty"` - NewUsername string `protobuf:"bytes,3,opt,name=new_username,json=newUsername,proto3" json:"new_username,omitempty"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + NewHash []byte `protobuf:"bytes,2,opt,name=new_hash,json=newHash,proto3" json:"new_hash,omitempty"` + NewUsername string `protobuf:"bytes,3,opt,name=new_username,json=newUsername,proto3" json:"new_username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdatePasswordReq) Reset() { *x = UpdatePasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdatePasswordReq) String() string { @@ -643,7 +627,7 @@ func (*UpdatePasswordReq) ProtoMessage() {} func (x *UpdatePasswordReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -681,20 +665,17 @@ func (x *UpdatePasswordReq) GetNewUsername() string { // UpdatePasswordResp returns the response from modifying an existing password. type UpdatePasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *UpdatePasswordResp) Reset() { *x = UpdatePasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdatePasswordResp) String() string { @@ -705,7 +686,7 @@ func (*UpdatePasswordResp) ProtoMessage() {} func (x *UpdatePasswordResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -729,20 +710,17 @@ func (x *UpdatePasswordResp) GetNotFound() bool { // DeletePasswordReq is a request to delete a password. type DeletePasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` unknownFields protoimpl.UnknownFields - - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeletePasswordReq) Reset() { *x = DeletePasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeletePasswordReq) String() string { @@ -753,7 +731,7 @@ func (*DeletePasswordReq) ProtoMessage() {} func (x *DeletePasswordReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -777,20 +755,17 @@ func (x *DeletePasswordReq) GetEmail() string { // DeletePasswordResp returns the response from deleting a password. type DeletePasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeletePasswordResp) Reset() { *x = DeletePasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeletePasswordResp) String() string { @@ -801,7 +776,7 @@ func (*DeletePasswordResp) ProtoMessage() {} func (x *DeletePasswordResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -825,18 +800,16 @@ func (x *DeletePasswordResp) GetNotFound() bool { // ListPasswordReq is a request to enumerate passwords. type ListPasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListPasswordReq) Reset() { *x = ListPasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListPasswordReq) String() string { @@ -847,7 +820,7 @@ func (*ListPasswordReq) ProtoMessage() {} func (x *ListPasswordReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -864,20 +837,17 @@ func (*ListPasswordReq) Descriptor() ([]byte, []int) { // ListPasswordResp returns a list of passwords. type ListPasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Passwords []*Password `protobuf:"bytes,1,rep,name=passwords,proto3" json:"passwords,omitempty"` unknownFields protoimpl.UnknownFields - - Passwords []*Password `protobuf:"bytes,1,rep,name=passwords,proto3" json:"passwords,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListPasswordResp) Reset() { *x = ListPasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListPasswordResp) String() string { @@ -888,7 +858,7 @@ func (*ListPasswordResp) ProtoMessage() {} func (x *ListPasswordResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -912,18 +882,16 @@ func (x *ListPasswordResp) GetPasswords() []*Password { // VersionReq is a request to fetch version info. type VersionReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VersionReq) Reset() { *x = VersionReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VersionReq) String() string { @@ -934,7 +902,7 @@ func (*VersionReq) ProtoMessage() {} func (x *VersionReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -951,24 +919,21 @@ func (*VersionReq) Descriptor() ([]byte, []int) { // VersionResp holds the version info of components. type VersionResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Semantic version of the server. Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` - // Numeric version of the API. It increases everytime a new call is added to the API. + // Numeric version of the API. It increases every time a new call is added to the API. // Clients should use this info to determine if the server supports specific features. - Api int32 `protobuf:"varint,2,opt,name=api,proto3" json:"api,omitempty"` + Api int32 `protobuf:"varint,2,opt,name=api,proto3" json:"api,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VersionResp) Reset() { *x = VersionResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VersionResp) String() string { @@ -979,7 +944,7 @@ func (*VersionResp) ProtoMessage() {} func (x *VersionResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1010,24 +975,21 @@ func (x *VersionResp) GetApi() int32 { // RefreshTokenRef contains the metadata for a refresh token that is managed by the storage. type RefreshTokenRef struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // ID of the refresh token. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - CreatedAt int64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - LastUsed int64 `protobuf:"varint,6,opt,name=last_used,json=lastUsed,proto3" json:"last_used,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + CreatedAt int64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastUsed int64 `protobuf:"varint,6,opt,name=last_used,json=lastUsed,proto3" json:"last_used,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RefreshTokenRef) Reset() { *x = RefreshTokenRef{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RefreshTokenRef) String() string { @@ -1038,7 +1000,7 @@ func (*RefreshTokenRef) ProtoMessage() {} func (x *RefreshTokenRef) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1083,21 +1045,18 @@ func (x *RefreshTokenRef) GetLastUsed() int64 { // ListRefreshReq is a request to enumerate the refresh tokens of a user. type ListRefreshReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The "sub" claim returned in the ID Token. - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListRefreshReq) Reset() { *x = ListRefreshReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListRefreshReq) String() string { @@ -1108,7 +1067,7 @@ func (*ListRefreshReq) ProtoMessage() {} func (x *ListRefreshReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1132,20 +1091,17 @@ func (x *ListRefreshReq) GetUserId() string { // ListRefreshResp returns a list of refresh tokens for a user. type ListRefreshResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RefreshTokens []*RefreshTokenRef `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"` unknownFields protoimpl.UnknownFields - - RefreshTokens []*RefreshTokenRef `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListRefreshResp) Reset() { *x = ListRefreshResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListRefreshResp) String() string { @@ -1156,7 +1112,7 @@ func (*ListRefreshResp) ProtoMessage() {} func (x *ListRefreshResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1180,22 +1136,19 @@ func (x *ListRefreshResp) GetRefreshTokens() []*RefreshTokenRef { // RevokeRefreshReq is a request to revoke the refresh token of the user-client pair. type RevokeRefreshReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The "sub" claim returned in the ID Token. - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RevokeRefreshReq) Reset() { *x = RevokeRefreshReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeRefreshReq) String() string { @@ -1206,7 +1159,7 @@ func (*RevokeRefreshReq) ProtoMessage() {} func (x *RevokeRefreshReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1237,21 +1190,18 @@ func (x *RevokeRefreshReq) GetClientId() string { // RevokeRefreshResp determines if the refresh token is revoked successfully. type RevokeRefreshResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Set to true is refresh token was not found and token could not be revoked. - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RevokeRefreshResp) Reset() { *x = RevokeRefreshResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeRefreshResp) String() string { @@ -1262,7 +1212,7 @@ func (*RevokeRefreshResp) ProtoMessage() {} func (x *RevokeRefreshResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1285,21 +1235,18 @@ func (x *RevokeRefreshResp) GetNotFound() bool { } type VerifyPasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields - - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + sizeCache protoimpl.SizeCache } func (x *VerifyPasswordReq) Reset() { *x = VerifyPasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VerifyPasswordReq) String() string { @@ -1310,7 +1257,7 @@ func (*VerifyPasswordReq) ProtoMessage() {} func (x *VerifyPasswordReq) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1340,21 +1287,18 @@ func (x *VerifyPasswordReq) GetPassword() string { } type VerifyPasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"` + NotFound bool `protobuf:"varint,2,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"` - NotFound bool `protobuf:"varint,2,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *VerifyPasswordResp) Reset() { *x = VerifyPasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_api_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_api_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VerifyPasswordResp) String() string { @@ -1365,7 +1309,7 @@ func (*VerifyPasswordResp) ProtoMessage() {} func (x *VerifyPasswordResp) ProtoReflect() protoreflect.Message { mi := &file_api_api_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1396,9 +1340,9 @@ func (x *VerifyPasswordResp) GetNotFound() bool { var File_api_api_proto protoreflect.FileDescriptor -var file_api_api_proto_rawDesc = []byte{ +var file_api_api_proto_rawDesc = string([]byte{ 0x0a, 0x0d, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x03, 0x61, 0x70, 0x69, 0x22, 0xc1, 0x01, 0x0a, 0x06, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, + 0x03, 0x61, 0x70, 0x69, 0x22, 0xf0, 0x01, 0x0a, 0x06, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x64, 0x69, 0x72, @@ -1410,171 +1354,177 @@ var file_api_api_proto_rawDesc = []byte{ 0x08, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x22, 0x36, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x23, 0x0a, 0x06, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x22, 0x5e, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, - 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, - 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x06, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x22, 0x21, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x22, 0x2f, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, - 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, - 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x9a, 0x01, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0c, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x69, 0x73, 0x12, 0x23, 0x0a, - 0x0d, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x65, 0x65, - 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, - 0x6c, 0x22, 0x2f, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, - 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, - 0x6e, 0x64, 0x22, 0x69, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x3e, 0x0a, - 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, - 0x65, 0x71, 0x12, 0x29, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3b, 0x0a, - 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x36, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x23, 0x0a, 0x06, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, + 0x5e, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x72, - 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x67, 0x0a, 0x11, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, - 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x68, 0x61, 0x73, - 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x48, 0x61, 0x73, 0x68, - 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x22, 0x31, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, - 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, - 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x29, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, - 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, - 0x6c, 0x22, 0x31, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x23, 0x0a, 0x06, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, + 0x21, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x22, 0x2f, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, + 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, + 0x75, 0x6e, 0x64, 0x22, 0xc9, 0x01, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x64, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, + 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x69, 0x73, 0x12, 0x23, 0x0a, 0x0d, + 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, + 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, + 0x2f, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, + 0x22, 0x69, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x3e, 0x0a, 0x11, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, + 0x12, 0x29, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3b, 0x0a, 0x12, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x65, 0x78, 0x69, + 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x72, 0x65, 0x61, + 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x67, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, + 0x61, 0x69, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, + 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x31, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, - 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x22, 0x3f, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2b, 0x0a, 0x09, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x09, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x22, 0x37, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x10, 0x0a, - 0x03, 0x61, 0x70, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x70, 0x69, 0x22, - 0x7a, 0x0a, 0x0f, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, - 0x65, 0x66, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, - 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0e, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, - 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x3b, 0x0a, 0x0e, 0x72, 0x65, 0x66, - 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66, 0x52, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, - 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, - 0x22, 0x30, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, + 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x29, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, + 0x31, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, - 0x6e, 0x64, 0x22, 0x45, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4d, 0x0a, 0x12, 0x56, 0x65, 0x72, - 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, - 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, - 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, - 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0xc7, 0x05, 0x0a, 0x03, 0x44, 0x65, 0x78, - 0x12, 0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, - 0x3d, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, - 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, - 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, - 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, - 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, - 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d, - 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, - 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, - 0x3a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x13, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, - 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, - 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x52, - 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x15, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, - 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, - 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, - 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, - 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, - 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x22, 0x00, 0x42, 0x2f, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, - 0x2e, 0x64, 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x19, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, - 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x6e, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x22, 0x3f, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2b, 0x0a, 0x09, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x09, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x22, 0x37, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x70, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x70, 0x69, 0x22, 0x7a, 0x0a, + 0x0f, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x08, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0e, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x3b, 0x0a, 0x0e, 0x72, 0x65, 0x66, 0x72, 0x65, + 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x52, 0x65, 0x66, 0x52, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x30, + 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, + 0x22, 0x45, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4d, 0x0a, 0x12, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1a, 0x0a, + 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, + 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, + 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0xc7, 0x05, 0x0a, 0x03, 0x44, 0x65, 0x78, 0x12, 0x3d, + 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, + 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, + 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, + 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0d, 0x4c, 0x69, + 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, + 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x47, 0x65, + 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3a, 0x0a, + 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x13, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, + 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x52, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, + 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, + 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x56, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, + 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, + 0x42, 0x2f, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, 0x2e, 0x64, + 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x19, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, 0x61, 0x70, + 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_api_api_proto_rawDescOnce sync.Once - file_api_api_proto_rawDescData = file_api_api_proto_rawDesc + file_api_api_proto_rawDescData []byte ) func file_api_api_proto_rawDescGZIP() []byte { file_api_api_proto_rawDescOnce.Do(func() { - file_api_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_api_proto_rawDescData) + file_api_api_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_api_proto_rawDesc), len(file_api_api_proto_rawDesc))) }) return file_api_api_proto_rawDescData } var file_api_api_proto_msgTypes = make([]protoimpl.MessageInfo, 25) -var file_api_api_proto_goTypes = []interface{}{ +var file_api_api_proto_goTypes = []any{ (*Client)(nil), // 0: api.Client (*CreateClientReq)(nil), // 1: api.CreateClientReq (*CreateClientResp)(nil), // 2: api.CreateClientResp @@ -1641,313 +1591,11 @@ func file_api_api_proto_init() { if File_api_api_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_api_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Client); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateClientReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateClientResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteClientReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteClientResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateClientReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateClientResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Password); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreatePasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreatePasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VersionReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VersionResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RefreshTokenRef); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRefreshReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRefreshResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeRefreshReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeRefreshResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VerifyPasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_api_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VerifyPasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_api_api_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_api_proto_rawDesc), len(file_api_api_proto_rawDesc)), NumEnums: 0, NumMessages: 25, NumExtensions: 0, @@ -1958,7 +1606,6 @@ func file_api_api_proto_init() { MessageInfos: file_api_api_proto_msgTypes, }.Build() File_api_api_proto = out.File - file_api_api_proto_rawDesc = nil file_api_api_proto_goTypes = nil file_api_api_proto_depIdxs = nil } diff --git a/api/api.proto b/api/api.proto index 7d25771a6e..01e3db1718 100644 --- a/api/api.proto +++ b/api/api.proto @@ -14,6 +14,7 @@ message Client { bool public = 5; string name = 6; string logo_url = 7; + repeated string allowed_connectors = 8; } // CreateClientReq is a request to make a client. @@ -45,6 +46,7 @@ message UpdateClientReq { repeated string trusted_peers = 3; string name = 4; string logo_url = 5; + repeated string allowed_connectors = 6; } // UpdateClientResp returns the response from updating a client. @@ -112,7 +114,7 @@ message VersionReq {} message VersionResp { // Semantic version of the server. string server = 1; - // Numeric version of the API. It increases everytime a new call is added to the API. + // Numeric version of the API. It increases every time a new call is added to the API. // Clients should use this info to determine if the server supports specific features. int32 api = 2; } diff --git a/api/api_grpc.pb.go b/api/api_grpc.pb.go index e8c9873cb5..aeeaa508c0 100644 --- a/api/api_grpc.pb.go +++ b/api/api_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: api/api.proto package api @@ -11,12 +15,28 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient" + Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient" + Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient" + Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword" + Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword" + Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword" + Dex_ListPasswords_FullMethodName = "/api.Dex/ListPasswords" + Dex_GetVersion_FullMethodName = "/api.Dex/GetVersion" + Dex_ListRefresh_FullMethodName = "/api.Dex/ListRefresh" + Dex_RevokeRefresh_FullMethodName = "/api.Dex/RevokeRefresh" + Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword" +) // DexClient is the client API for Dex service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Dex represents the dex gRPC service. type DexClient interface { // CreateClient creates a client. CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error) @@ -53,8 +73,9 @@ func NewDexClient(cc grpc.ClientConnInterface) DexClient { } func (c *dexClient) CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreateClientResp) - err := c.cc.Invoke(ctx, "/api.Dex/CreateClient", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_CreateClient_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -62,8 +83,9 @@ func (c *dexClient) CreateClient(ctx context.Context, in *CreateClientReq, opts } func (c *dexClient) UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UpdateClientResp) - err := c.cc.Invoke(ctx, "/api.Dex/UpdateClient", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_UpdateClient_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -71,8 +93,9 @@ func (c *dexClient) UpdateClient(ctx context.Context, in *UpdateClientReq, opts } func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteClientResp) - err := c.cc.Invoke(ctx, "/api.Dex/DeleteClient", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_DeleteClient_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -80,8 +103,9 @@ func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts } func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreatePasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/CreatePassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_CreatePassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -89,8 +113,9 @@ func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, o } func (c *dexClient) UpdatePassword(ctx context.Context, in *UpdatePasswordReq, opts ...grpc.CallOption) (*UpdatePasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UpdatePasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/UpdatePassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_UpdatePassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -98,8 +123,9 @@ func (c *dexClient) UpdatePassword(ctx context.Context, in *UpdatePasswordReq, o } func (c *dexClient) DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeletePasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/DeletePassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_DeletePassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -107,8 +133,9 @@ func (c *dexClient) DeletePassword(ctx context.Context, in *DeletePasswordReq, o } func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListPasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/ListPasswords", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_ListPasswords_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -116,8 +143,9 @@ func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts } func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(VersionResp) - err := c.cc.Invoke(ctx, "/api.Dex/GetVersion", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_GetVersion_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -125,8 +153,9 @@ func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc } func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListRefreshResp) - err := c.cc.Invoke(ctx, "/api.Dex/ListRefresh", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_ListRefresh_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -134,8 +163,9 @@ func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts .. } func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RevokeRefreshResp) - err := c.cc.Invoke(ctx, "/api.Dex/RevokeRefresh", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_RevokeRefresh_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -143,8 +173,9 @@ func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opt } func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(VerifyPasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/VerifyPassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_VerifyPassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -153,7 +184,9 @@ func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, o // DexServer is the server API for Dex service. // All implementations must embed UnimplementedDexServer -// for forward compatibility +// for forward compatibility. +// +// Dex represents the dex gRPC service. type DexServer interface { // CreateClient creates a client. CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error) @@ -182,9 +215,12 @@ type DexServer interface { mustEmbedUnimplementedDexServer() } -// UnimplementedDexServer must be embedded to have forward compatible implementations. -type UnimplementedDexServer struct { -} +// UnimplementedDexServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDexServer struct{} func (UnimplementedDexServer) CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateClient not implemented") @@ -220,6 +256,7 @@ func (UnimplementedDexServer) VerifyPassword(context.Context, *VerifyPasswordReq return nil, status.Errorf(codes.Unimplemented, "method VerifyPassword not implemented") } func (UnimplementedDexServer) mustEmbedUnimplementedDexServer() {} +func (UnimplementedDexServer) testEmbeddedByValue() {} // UnsafeDexServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to DexServer will @@ -229,6 +266,13 @@ type UnsafeDexServer interface { } func RegisterDexServer(s grpc.ServiceRegistrar, srv DexServer) { + // If the following call pancis, it indicates UnimplementedDexServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&Dex_ServiceDesc, srv) } @@ -242,7 +286,7 @@ func _Dex_CreateClient_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/CreateClient", + FullMethod: Dex_CreateClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).CreateClient(ctx, req.(*CreateClientReq)) @@ -260,7 +304,7 @@ func _Dex_UpdateClient_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/UpdateClient", + FullMethod: Dex_UpdateClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).UpdateClient(ctx, req.(*UpdateClientReq)) @@ -278,7 +322,7 @@ func _Dex_DeleteClient_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/DeleteClient", + FullMethod: Dex_DeleteClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).DeleteClient(ctx, req.(*DeleteClientReq)) @@ -296,7 +340,7 @@ func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/CreatePassword", + FullMethod: Dex_CreatePassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).CreatePassword(ctx, req.(*CreatePasswordReq)) @@ -314,7 +358,7 @@ func _Dex_UpdatePassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/UpdatePassword", + FullMethod: Dex_UpdatePassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).UpdatePassword(ctx, req.(*UpdatePasswordReq)) @@ -332,7 +376,7 @@ func _Dex_DeletePassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/DeletePassword", + FullMethod: Dex_DeletePassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).DeletePassword(ctx, req.(*DeletePasswordReq)) @@ -350,7 +394,7 @@ func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/ListPasswords", + FullMethod: Dex_ListPasswords_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).ListPasswords(ctx, req.(*ListPasswordReq)) @@ -368,7 +412,7 @@ func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(inte } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/GetVersion", + FullMethod: Dex_GetVersion_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).GetVersion(ctx, req.(*VersionReq)) @@ -386,7 +430,7 @@ func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/ListRefresh", + FullMethod: Dex_ListRefresh_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).ListRefresh(ctx, req.(*ListRefreshReq)) @@ -404,7 +448,7 @@ func _Dex_RevokeRefresh_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/RevokeRefresh", + FullMethod: Dex_RevokeRefresh_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).RevokeRefresh(ctx, req.(*RevokeRefreshReq)) @@ -422,7 +466,7 @@ func _Dex_VerifyPassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/VerifyPassword", + FullMethod: Dex_VerifyPassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).VerifyPassword(ctx, req.(*VerifyPasswordReq)) diff --git a/api/v2/api.pb.go b/api/v2/api.pb.go index f49310f311..8495519008 100644 --- a/api/v2/api.pb.go +++ b/api/v2/api.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.26.0 -// protoc v3.15.6 +// protoc-gen-go v1.36.5 +// protoc v5.29.3 // source: api/v2/api.proto package api @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,26 +23,24 @@ const ( // Client represents an OAuth2 client. type Client struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - RedirectUris []string `protobuf:"bytes,3,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` - TrustedPeers []string `protobuf:"bytes,4,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` - Public bool `protobuf:"varint,5,opt,name=public,proto3" json:"public,omitempty"` - Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` - LogoUrl string `protobuf:"bytes,7,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + RedirectUris []string `protobuf:"bytes,3,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,4,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Public bool `protobuf:"varint,5,opt,name=public,proto3" json:"public,omitempty"` + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,7,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + AllowedConnectors []string `protobuf:"bytes,8,rep,name=allowed_connectors,json=allowedConnectors,proto3" json:"allowed_connectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Client) Reset() { *x = Client{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Client) String() string { @@ -52,7 +51,7 @@ func (*Client) ProtoMessage() {} func (x *Client) ProtoReflect() protoreflect.Message { mi := &file_api_v2_api_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -116,22 +115,210 @@ func (x *Client) GetLogoUrl() string { return "" } -// CreateClientReq is a request to make a client. -type CreateClientReq struct { - state protoimpl.MessageState +func (x *Client) GetAllowedConnectors() []string { + if x != nil { + return x.AllowedConnectors + } + return nil +} + +// ClientInfo represents an OAuth2 client without sensitive information. +type ClientInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Public bool `protobuf:"varint,4,opt,name=public,proto3" json:"public,omitempty"` + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,6,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + AllowedConnectors []string `protobuf:"bytes,7,rep,name=allowed_connectors,json=allowedConnectors,proto3" json:"allowed_connectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientInfo) Reset() { + *x = ClientInfo{} + mi := &file_api_v2_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientInfo) ProtoMessage() {} + +func (x *ClientInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientInfo.ProtoReflect.Descriptor instead. +func (*ClientInfo) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{1} +} + +func (x *ClientInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ClientInfo) GetRedirectUris() []string { + if x != nil { + return x.RedirectUris + } + return nil +} + +func (x *ClientInfo) GetTrustedPeers() []string { + if x != nil { + return x.TrustedPeers + } + return nil +} + +func (x *ClientInfo) GetPublic() bool { + if x != nil { + return x.Public + } + return false +} + +func (x *ClientInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ClientInfo) GetLogoUrl() string { + if x != nil { + return x.LogoUrl + } + return "" +} + +func (x *ClientInfo) GetAllowedConnectors() []string { + if x != nil { + return x.AllowedConnectors + } + return nil +} + +// GetClientReq is a request to retrieve client details. +type GetClientReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The ID of the client. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache +} + +func (x *GetClientReq) Reset() { + *x = GetClientReq{} + mi := &file_api_v2_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetClientReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetClientReq) ProtoMessage() {} + +func (x *GetClientReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClientReq.ProtoReflect.Descriptor instead. +func (*GetClientReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{2} +} + +func (x *GetClientReq) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetClientResp returns the client details. +type GetClientResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetClientResp) Reset() { + *x = GetClientResp{} + mi := &file_api_v2_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} - Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"` +func (x *GetClientResp) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *CreateClientReq) Reset() { - *x = CreateClientReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[1] +func (*GetClientResp) ProtoMessage() {} + +func (x *GetClientResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[3] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetClientResp.ProtoReflect.Descriptor instead. +func (*GetClientResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{3} +} + +func (x *GetClientResp) GetClient() *Client { + if x != nil { + return x.Client } + return nil +} + +// CreateClientReq is a request to make a client. +type CreateClientReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateClientReq) Reset() { + *x = CreateClientReq{} + mi := &file_api_v2_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateClientReq) String() string { @@ -141,8 +328,8 @@ func (x *CreateClientReq) String() string { func (*CreateClientReq) ProtoMessage() {} func (x *CreateClientReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[4] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -154,7 +341,7 @@ func (x *CreateClientReq) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateClientReq.ProtoReflect.Descriptor instead. func (*CreateClientReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{1} + return file_api_v2_api_proto_rawDescGZIP(), []int{4} } func (x *CreateClientReq) GetClient() *Client { @@ -166,21 +353,18 @@ func (x *CreateClientReq) GetClient() *Client { // CreateClientResp returns the response from creating a client. type CreateClientResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` + Client *Client `protobuf:"bytes,2,opt,name=client,proto3" json:"client,omitempty"` unknownFields protoimpl.UnknownFields - - AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` - Client *Client `protobuf:"bytes,2,opt,name=client,proto3" json:"client,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreateClientResp) Reset() { *x = CreateClientResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreateClientResp) String() string { @@ -190,8 +374,8 @@ func (x *CreateClientResp) String() string { func (*CreateClientResp) ProtoMessage() {} func (x *CreateClientResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[5] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -203,7 +387,7 @@ func (x *CreateClientResp) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateClientResp.ProtoReflect.Descriptor instead. func (*CreateClientResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{2} + return file_api_v2_api_proto_rawDescGZIP(), []int{5} } func (x *CreateClientResp) GetAlreadyExists() bool { @@ -222,21 +406,18 @@ func (x *CreateClientResp) GetClient() *Client { // DeleteClientReq is a request to delete a client. type DeleteClientReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The ID of the client. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DeleteClientReq) Reset() { *x = DeleteClientReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteClientReq) String() string { @@ -246,8 +427,8 @@ func (x *DeleteClientReq) String() string { func (*DeleteClientReq) ProtoMessage() {} func (x *DeleteClientReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[6] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -259,7 +440,7 @@ func (x *DeleteClientReq) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteClientReq.ProtoReflect.Descriptor instead. func (*DeleteClientReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{3} + return file_api_v2_api_proto_rawDescGZIP(), []int{6} } func (x *DeleteClientReq) GetId() string { @@ -271,20 +452,17 @@ func (x *DeleteClientReq) GetId() string { // DeleteClientResp determines if the client is deleted successfully. type DeleteClientResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteClientResp) Reset() { *x = DeleteClientResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteClientResp) String() string { @@ -294,8 +472,8 @@ func (x *DeleteClientResp) String() string { func (*DeleteClientResp) ProtoMessage() {} func (x *DeleteClientResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[7] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -307,7 +485,7 @@ func (x *DeleteClientResp) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteClientResp.ProtoReflect.Descriptor instead. func (*DeleteClientResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{4} + return file_api_v2_api_proto_rawDescGZIP(), []int{7} } func (x *DeleteClientResp) GetNotFound() bool { @@ -319,24 +497,22 @@ func (x *DeleteClientResp) GetNotFound() bool { // UpdateClientReq is a request to update an existing client. type UpdateClientReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` - TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` - LogoUrl string `protobuf:"bytes,5,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,5,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + AllowedConnectors []string `protobuf:"bytes,6,rep,name=allowed_connectors,json=allowedConnectors,proto3" json:"allowed_connectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateClientReq) Reset() { *x = UpdateClientReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdateClientReq) String() string { @@ -346,8 +522,8 @@ func (x *UpdateClientReq) String() string { func (*UpdateClientReq) ProtoMessage() {} func (x *UpdateClientReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[8] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -359,7 +535,7 @@ func (x *UpdateClientReq) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateClientReq.ProtoReflect.Descriptor instead. func (*UpdateClientReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{5} + return file_api_v2_api_proto_rawDescGZIP(), []int{8} } func (x *UpdateClientReq) GetId() string { @@ -397,22 +573,26 @@ func (x *UpdateClientReq) GetLogoUrl() string { return "" } +func (x *UpdateClientReq) GetAllowedConnectors() []string { + if x != nil { + return x.AllowedConnectors + } + return nil +} + // UpdateClientResp returns the response from updating a client. type UpdateClientResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *UpdateClientResp) Reset() { *x = UpdateClientResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdateClientResp) String() string { @@ -422,8 +602,8 @@ func (x *UpdateClientResp) String() string { func (*UpdateClientResp) ProtoMessage() {} func (x *UpdateClientResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[9] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -435,7 +615,7 @@ func (x *UpdateClientResp) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateClientResp.ProtoReflect.Descriptor instead. func (*UpdateClientResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{6} + return file_api_v2_api_proto_rawDescGZIP(), []int{9} } func (x *UpdateClientResp) GetNotFound() bool { @@ -445,37 +625,29 @@ func (x *UpdateClientResp) GetNotFound() bool { return false } -// Password is an email for password mapping managed by the storage. -type Password struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// ListClientReq is a request to enumerate clients. +type ListClientReq struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields - - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - // Currently we do not accept plain text passwords. Could be an option in the future. - Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` - Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` - UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + sizeCache protoimpl.SizeCache } -func (x *Password) Reset() { - *x = Password{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *ListClientReq) Reset() { + *x = ListClientReq{} + mi := &file_api_v2_api_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *Password) String() string { +func (x *ListClientReq) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Password) ProtoMessage() {} +func (*ListClientReq) ProtoMessage() {} -func (x *Password) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { +func (x *ListClientReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[10] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -485,55 +657,139 @@ func (x *Password) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Password.ProtoReflect.Descriptor instead. -func (*Password) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{7} +// Deprecated: Use ListClientReq.ProtoReflect.Descriptor instead. +func (*ListClientReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{10} } -func (x *Password) GetEmail() string { - if x != nil { - return x.Email - } - return "" +// ListClientResp returns a list of clients. +type ListClientResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Clients []*ClientInfo `protobuf:"bytes,1,rep,name=clients,proto3" json:"clients,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Password) GetHash() []byte { - if x != nil { - return x.Hash - } - return nil +func (x *ListClientResp) Reset() { + *x = ListClientResp{} + mi := &file_api_v2_api_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *Password) GetUsername() string { - if x != nil { - return x.Username - } - return "" +func (x *ListClientResp) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *Password) GetUserId() string { +func (*ListClientResp) ProtoMessage() {} + +func (x *ListClientResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[11] if x != nil { - return x.UserId + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListClientResp.ProtoReflect.Descriptor instead. +func (*ListClientResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{11} +} + +func (x *ListClientResp) GetClients() []*ClientInfo { + if x != nil { + return x.Clients + } + return nil +} + +// Password is an email for password mapping managed by the storage. +type Password struct { + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + // Currently we do not accept plain text passwords. Could be an option in the future. + Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Password) Reset() { + *x = Password{} + mi := &file_api_v2_api_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Password) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Password) ProtoMessage() {} + +func (x *Password) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Password.ProtoReflect.Descriptor instead. +func (*Password) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{12} +} + +func (x *Password) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *Password) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +func (x *Password) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *Password) GetUserId() string { + if x != nil { + return x.UserId } return "" } // CreatePasswordReq is a request to make a password. type CreatePasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Password *Password `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields - - Password *Password `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreatePasswordReq) Reset() { *x = CreatePasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreatePasswordReq) String() string { @@ -543,8 +799,8 @@ func (x *CreatePasswordReq) String() string { func (*CreatePasswordReq) ProtoMessage() {} func (x *CreatePasswordReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[13] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -556,7 +812,7 @@ func (x *CreatePasswordReq) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatePasswordReq.ProtoReflect.Descriptor instead. func (*CreatePasswordReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{8} + return file_api_v2_api_proto_rawDescGZIP(), []int{13} } func (x *CreatePasswordReq) GetPassword() *Password { @@ -568,20 +824,17 @@ func (x *CreatePasswordReq) GetPassword() *Password { // CreatePasswordResp returns the response from creating a password. type CreatePasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` unknownFields protoimpl.UnknownFields - - AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CreatePasswordResp) Reset() { *x = CreatePasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CreatePasswordResp) String() string { @@ -591,8 +844,8 @@ func (x *CreatePasswordResp) String() string { func (*CreatePasswordResp) ProtoMessage() {} func (x *CreatePasswordResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[14] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -604,7 +857,7 @@ func (x *CreatePasswordResp) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatePasswordResp.ProtoReflect.Descriptor instead. func (*CreatePasswordResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{9} + return file_api_v2_api_proto_rawDescGZIP(), []int{14} } func (x *CreatePasswordResp) GetAlreadyExists() bool { @@ -616,23 +869,20 @@ func (x *CreatePasswordResp) GetAlreadyExists() bool { // UpdatePasswordReq is a request to modify an existing password. type UpdatePasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The email used to lookup the password. This field cannot be modified - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - NewHash []byte `protobuf:"bytes,2,opt,name=new_hash,json=newHash,proto3" json:"new_hash,omitempty"` - NewUsername string `protobuf:"bytes,3,opt,name=new_username,json=newUsername,proto3" json:"new_username,omitempty"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + NewHash []byte `protobuf:"bytes,2,opt,name=new_hash,json=newHash,proto3" json:"new_hash,omitempty"` + NewUsername string `protobuf:"bytes,3,opt,name=new_username,json=newUsername,proto3" json:"new_username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdatePasswordReq) Reset() { *x = UpdatePasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdatePasswordReq) String() string { @@ -642,8 +892,8 @@ func (x *UpdatePasswordReq) String() string { func (*UpdatePasswordReq) ProtoMessage() {} func (x *UpdatePasswordReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[15] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -655,7 +905,7 @@ func (x *UpdatePasswordReq) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePasswordReq.ProtoReflect.Descriptor instead. func (*UpdatePasswordReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{10} + return file_api_v2_api_proto_rawDescGZIP(), []int{15} } func (x *UpdatePasswordReq) GetEmail() string { @@ -681,20 +931,17 @@ func (x *UpdatePasswordReq) GetNewUsername() string { // UpdatePasswordResp returns the response from modifying an existing password. type UpdatePasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *UpdatePasswordResp) Reset() { *x = UpdatePasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdatePasswordResp) String() string { @@ -704,8 +951,8 @@ func (x *UpdatePasswordResp) String() string { func (*UpdatePasswordResp) ProtoMessage() {} func (x *UpdatePasswordResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[16] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -717,7 +964,7 @@ func (x *UpdatePasswordResp) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePasswordResp.ProtoReflect.Descriptor instead. func (*UpdatePasswordResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{11} + return file_api_v2_api_proto_rawDescGZIP(), []int{16} } func (x *UpdatePasswordResp) GetNotFound() bool { @@ -729,20 +976,17 @@ func (x *UpdatePasswordResp) GetNotFound() bool { // DeletePasswordReq is a request to delete a password. type DeletePasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` unknownFields protoimpl.UnknownFields - - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeletePasswordReq) Reset() { *x = DeletePasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeletePasswordReq) String() string { @@ -752,8 +996,8 @@ func (x *DeletePasswordReq) String() string { func (*DeletePasswordReq) ProtoMessage() {} func (x *DeletePasswordReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[17] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -765,7 +1009,7 @@ func (x *DeletePasswordReq) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePasswordReq.ProtoReflect.Descriptor instead. func (*DeletePasswordReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{12} + return file_api_v2_api_proto_rawDescGZIP(), []int{17} } func (x *DeletePasswordReq) GetEmail() string { @@ -777,20 +1021,17 @@ func (x *DeletePasswordReq) GetEmail() string { // DeletePasswordResp returns the response from deleting a password. type DeletePasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeletePasswordResp) Reset() { *x = DeletePasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeletePasswordResp) String() string { @@ -800,8 +1041,8 @@ func (x *DeletePasswordResp) String() string { func (*DeletePasswordResp) ProtoMessage() {} func (x *DeletePasswordResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[18] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -813,41 +1054,550 @@ func (x *DeletePasswordResp) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePasswordResp.ProtoReflect.Descriptor instead. func (*DeletePasswordResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{13} + return file_api_v2_api_proto_rawDescGZIP(), []int{18} +} + +func (x *DeletePasswordResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// ListPasswordReq is a request to enumerate passwords. +type ListPasswordReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPasswordReq) Reset() { + *x = ListPasswordReq{} + mi := &file_api_v2_api_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPasswordReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPasswordReq) ProtoMessage() {} + +func (x *ListPasswordReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPasswordReq.ProtoReflect.Descriptor instead. +func (*ListPasswordReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{19} +} + +// ListPasswordResp returns a list of passwords. +type ListPasswordResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Passwords []*Password `protobuf:"bytes,1,rep,name=passwords,proto3" json:"passwords,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPasswordResp) Reset() { + *x = ListPasswordResp{} + mi := &file_api_v2_api_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPasswordResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPasswordResp) ProtoMessage() {} + +func (x *ListPasswordResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPasswordResp.ProtoReflect.Descriptor instead. +func (*ListPasswordResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{20} +} + +func (x *ListPasswordResp) GetPasswords() []*Password { + if x != nil { + return x.Passwords + } + return nil +} + +// Connector is a strategy used by Dex for authenticating a user against another identity provider +type Connector struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` + GrantTypes []string `protobuf:"bytes,5,rep,name=grant_types,json=grantTypes,proto3" json:"grant_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Connector) Reset() { + *x = Connector{} + mi := &file_api_v2_api_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Connector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connector) ProtoMessage() {} + +func (x *Connector) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Connector.ProtoReflect.Descriptor instead. +func (*Connector) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{21} +} + +func (x *Connector) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Connector) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Connector) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Connector) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +func (x *Connector) GetGrantTypes() []string { + if x != nil { + return x.GrantTypes + } + return nil +} + +// CreateConnectorReq is a request to make a connector. +type CreateConnectorReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Connector *Connector `protobuf:"bytes,1,opt,name=connector,proto3" json:"connector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateConnectorReq) Reset() { + *x = CreateConnectorReq{} + mi := &file_api_v2_api_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateConnectorReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateConnectorReq) ProtoMessage() {} + +func (x *CreateConnectorReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateConnectorReq.ProtoReflect.Descriptor instead. +func (*CreateConnectorReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{22} +} + +func (x *CreateConnectorReq) GetConnector() *Connector { + if x != nil { + return x.Connector + } + return nil +} + +// CreateConnectorResp returns the response from creating a connector. +type CreateConnectorResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + AlreadyExists bool `protobuf:"varint,1,opt,name=already_exists,json=alreadyExists,proto3" json:"already_exists,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateConnectorResp) Reset() { + *x = CreateConnectorResp{} + mi := &file_api_v2_api_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateConnectorResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateConnectorResp) ProtoMessage() {} + +func (x *CreateConnectorResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateConnectorResp.ProtoReflect.Descriptor instead. +func (*CreateConnectorResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{23} +} + +func (x *CreateConnectorResp) GetAlreadyExists() bool { + if x != nil { + return x.AlreadyExists + } + return false +} + +// GrantTypes wraps a list of grant types to distinguish between +// "not specified" (no update) and "empty list" (unrestricted). +type GrantTypes struct { + state protoimpl.MessageState `protogen:"open.v1"` + GrantTypes []string `protobuf:"bytes,1,rep,name=grant_types,json=grantTypes,proto3" json:"grant_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GrantTypes) Reset() { + *x = GrantTypes{} + mi := &file_api_v2_api_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GrantTypes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrantTypes) ProtoMessage() {} + +func (x *GrantTypes) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrantTypes.ProtoReflect.Descriptor instead. +func (*GrantTypes) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{24} +} + +func (x *GrantTypes) GetGrantTypes() []string { + if x != nil { + return x.GrantTypes + } + return nil +} + +// UpdateConnectorReq is a request to modify an existing connector. +type UpdateConnectorReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The id used to lookup the connector. This field cannot be modified + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + NewType string `protobuf:"bytes,2,opt,name=new_type,json=newType,proto3" json:"new_type,omitempty"` + NewName string `protobuf:"bytes,3,opt,name=new_name,json=newName,proto3" json:"new_name,omitempty"` + NewConfig []byte `protobuf:"bytes,4,opt,name=new_config,json=newConfig,proto3" json:"new_config,omitempty"` + // If set, updates the connector's allowed grant types. + // An empty grant_types list means unrestricted (all grant types allowed). + // If not set (null), grant types are not modified. + NewGrantTypes *GrantTypes `protobuf:"bytes,5,opt,name=new_grant_types,json=newGrantTypes,proto3" json:"new_grant_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateConnectorReq) Reset() { + *x = UpdateConnectorReq{} + mi := &file_api_v2_api_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateConnectorReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateConnectorReq) ProtoMessage() {} + +func (x *UpdateConnectorReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateConnectorReq.ProtoReflect.Descriptor instead. +func (*UpdateConnectorReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{25} +} + +func (x *UpdateConnectorReq) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateConnectorReq) GetNewType() string { + if x != nil { + return x.NewType + } + return "" +} + +func (x *UpdateConnectorReq) GetNewName() string { + if x != nil { + return x.NewName + } + return "" +} + +func (x *UpdateConnectorReq) GetNewConfig() []byte { + if x != nil { + return x.NewConfig + } + return nil +} + +func (x *UpdateConnectorReq) GetNewGrantTypes() *GrantTypes { + if x != nil { + return x.NewGrantTypes + } + return nil +} + +// UpdateConnectorResp returns the response from modifying an existing connector. +type UpdateConnectorResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateConnectorResp) Reset() { + *x = UpdateConnectorResp{} + mi := &file_api_v2_api_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateConnectorResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateConnectorResp) ProtoMessage() {} + +func (x *UpdateConnectorResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateConnectorResp.ProtoReflect.Descriptor instead. +func (*UpdateConnectorResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{26} +} + +func (x *UpdateConnectorResp) GetNotFound() bool { + if x != nil { + return x.NotFound + } + return false +} + +// DeleteConnectorReq is a request to delete a connector. +type DeleteConnectorReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteConnectorReq) Reset() { + *x = DeleteConnectorReq{} + mi := &file_api_v2_api_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteConnectorReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteConnectorReq) ProtoMessage() {} + +func (x *DeleteConnectorReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteConnectorReq.ProtoReflect.Descriptor instead. +func (*DeleteConnectorReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{27} +} + +func (x *DeleteConnectorReq) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// DeleteConnectorResp returns the response from deleting a connector. +type DeleteConnectorResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteConnectorResp) Reset() { + *x = DeleteConnectorResp{} + mi := &file_api_v2_api_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteConnectorResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteConnectorResp) ProtoMessage() {} + +func (x *DeleteConnectorResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteConnectorResp.ProtoReflect.Descriptor instead. +func (*DeleteConnectorResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{28} } -func (x *DeletePasswordResp) GetNotFound() bool { +func (x *DeleteConnectorResp) GetNotFound() bool { if x != nil { return x.NotFound } return false } -// ListPasswordReq is a request to enumerate passwords. -type ListPasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// ListConnectorReq is a request to enumerate connectors. +type ListConnectorReq struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ListPasswordReq) Reset() { - *x = ListPasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *ListConnectorReq) Reset() { + *x = ListConnectorReq{} + mi := &file_api_v2_api_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *ListPasswordReq) String() string { +func (x *ListConnectorReq) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListPasswordReq) ProtoMessage() {} +func (*ListConnectorReq) ProtoMessage() {} -func (x *ListPasswordReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { +func (x *ListConnectorReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[29] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -857,38 +1607,35 @@ func (x *ListPasswordReq) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListPasswordReq.ProtoReflect.Descriptor instead. -func (*ListPasswordReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{14} +// Deprecated: Use ListConnectorReq.ProtoReflect.Descriptor instead. +func (*ListConnectorReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{29} } -// ListPasswordResp returns a list of passwords. -type ListPasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// ListConnectorResp returns a list of connectors. +type ListConnectorResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Connectors []*Connector `protobuf:"bytes,1,rep,name=connectors,proto3" json:"connectors,omitempty"` unknownFields protoimpl.UnknownFields - - Passwords []*Password `protobuf:"bytes,1,rep,name=passwords,proto3" json:"passwords,omitempty"` + sizeCache protoimpl.SizeCache } -func (x *ListPasswordResp) Reset() { - *x = ListPasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *ListConnectorResp) Reset() { + *x = ListConnectorResp{} + mi := &file_api_v2_api_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *ListPasswordResp) String() string { +func (x *ListConnectorResp) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListPasswordResp) ProtoMessage() {} +func (*ListConnectorResp) ProtoMessage() {} -func (x *ListPasswordResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { +func (x *ListConnectorResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[30] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -898,32 +1645,30 @@ func (x *ListPasswordResp) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListPasswordResp.ProtoReflect.Descriptor instead. -func (*ListPasswordResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{15} +// Deprecated: Use ListConnectorResp.ProtoReflect.Descriptor instead. +func (*ListConnectorResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{30} } -func (x *ListPasswordResp) GetPasswords() []*Password { +func (x *ListConnectorResp) GetConnectors() []*Connector { if x != nil { - return x.Passwords + return x.Connectors } return nil } // VersionReq is a request to fetch version info. type VersionReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VersionReq) Reset() { *x = VersionReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VersionReq) String() string { @@ -933,8 +1678,8 @@ func (x *VersionReq) String() string { func (*VersionReq) ProtoMessage() {} func (x *VersionReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[31] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -946,29 +1691,26 @@ func (x *VersionReq) ProtoReflect() protoreflect.Message { // Deprecated: Use VersionReq.ProtoReflect.Descriptor instead. func (*VersionReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{16} + return file_api_v2_api_proto_rawDescGZIP(), []int{31} } // VersionResp holds the version info of components. type VersionResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Semantic version of the server. Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` - // Numeric version of the API. It increases everytime a new call is added to the API. + // Numeric version of the API. It increases every time a new call is added to the API. // Clients should use this info to determine if the server supports specific features. - Api int32 `protobuf:"varint,2,opt,name=api,proto3" json:"api,omitempty"` + Api int32 `protobuf:"varint,2,opt,name=api,proto3" json:"api,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VersionResp) Reset() { *x = VersionResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VersionResp) String() string { @@ -978,8 +1720,8 @@ func (x *VersionResp) String() string { func (*VersionResp) ProtoMessage() {} func (x *VersionResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[32] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -991,7 +1733,7 @@ func (x *VersionResp) ProtoReflect() protoreflect.Message { // Deprecated: Use VersionResp.ProtoReflect.Descriptor instead. func (*VersionResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{17} + return file_api_v2_api_proto_rawDescGZIP(), []int{32} } func (x *VersionResp) GetServer() string { @@ -1008,26 +1750,217 @@ func (x *VersionResp) GetApi() int32 { return 0 } -// RefreshTokenRef contains the metadata for a refresh token that is managed by the storage. -type RefreshTokenRef struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// DiscoveryReq is a request to fetch discover information. +type DiscoveryReq struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiscoveryReq) Reset() { + *x = DiscoveryReq{} + mi := &file_api_v2_api_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiscoveryReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiscoveryReq) ProtoMessage() {} + +func (x *DiscoveryReq) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiscoveryReq.ProtoReflect.Descriptor instead. +func (*DiscoveryReq) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{33} +} + +// DiscoverResp holds the version oidc disovery info. +type DiscoveryResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Issuer string `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"` + AuthorizationEndpoint string `protobuf:"bytes,2,opt,name=authorization_endpoint,json=authorizationEndpoint,proto3" json:"authorization_endpoint,omitempty"` + TokenEndpoint string `protobuf:"bytes,3,opt,name=token_endpoint,json=tokenEndpoint,proto3" json:"token_endpoint,omitempty"` + JwksUri string `protobuf:"bytes,4,opt,name=jwks_uri,json=jwksUri,proto3" json:"jwks_uri,omitempty"` + UserinfoEndpoint string `protobuf:"bytes,5,opt,name=userinfo_endpoint,json=userinfoEndpoint,proto3" json:"userinfo_endpoint,omitempty"` + DeviceAuthorizationEndpoint string `protobuf:"bytes,6,opt,name=device_authorization_endpoint,json=deviceAuthorizationEndpoint,proto3" json:"device_authorization_endpoint,omitempty"` + IntrospectionEndpoint string `protobuf:"bytes,7,opt,name=introspection_endpoint,json=introspectionEndpoint,proto3" json:"introspection_endpoint,omitempty"` + GrantTypesSupported []string `protobuf:"bytes,8,rep,name=grant_types_supported,json=grantTypesSupported,proto3" json:"grant_types_supported,omitempty"` + ResponseTypesSupported []string `protobuf:"bytes,9,rep,name=response_types_supported,json=responseTypesSupported,proto3" json:"response_types_supported,omitempty"` + SubjectTypesSupported []string `protobuf:"bytes,10,rep,name=subject_types_supported,json=subjectTypesSupported,proto3" json:"subject_types_supported,omitempty"` + IdTokenSigningAlgValuesSupported []string `protobuf:"bytes,11,rep,name=id_token_signing_alg_values_supported,json=idTokenSigningAlgValuesSupported,proto3" json:"id_token_signing_alg_values_supported,omitempty"` + CodeChallengeMethodsSupported []string `protobuf:"bytes,12,rep,name=code_challenge_methods_supported,json=codeChallengeMethodsSupported,proto3" json:"code_challenge_methods_supported,omitempty"` + ScopesSupported []string `protobuf:"bytes,13,rep,name=scopes_supported,json=scopesSupported,proto3" json:"scopes_supported,omitempty"` + TokenEndpointAuthMethodsSupported []string `protobuf:"bytes,14,rep,name=token_endpoint_auth_methods_supported,json=tokenEndpointAuthMethodsSupported,proto3" json:"token_endpoint_auth_methods_supported,omitempty"` + ClaimsSupported []string `protobuf:"bytes,15,rep,name=claims_supported,json=claimsSupported,proto3" json:"claims_supported,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DiscoveryResp) Reset() { + *x = DiscoveryResp{} + mi := &file_api_v2_api_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DiscoveryResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DiscoveryResp) ProtoMessage() {} + +func (x *DiscoveryResp) ProtoReflect() protoreflect.Message { + mi := &file_api_v2_api_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DiscoveryResp.ProtoReflect.Descriptor instead. +func (*DiscoveryResp) Descriptor() ([]byte, []int) { + return file_api_v2_api_proto_rawDescGZIP(), []int{34} +} + +func (x *DiscoveryResp) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +func (x *DiscoveryResp) GetAuthorizationEndpoint() string { + if x != nil { + return x.AuthorizationEndpoint + } + return "" +} + +func (x *DiscoveryResp) GetTokenEndpoint() string { + if x != nil { + return x.TokenEndpoint + } + return "" +} + +func (x *DiscoveryResp) GetJwksUri() string { + if x != nil { + return x.JwksUri + } + return "" +} + +func (x *DiscoveryResp) GetUserinfoEndpoint() string { + if x != nil { + return x.UserinfoEndpoint + } + return "" +} + +func (x *DiscoveryResp) GetDeviceAuthorizationEndpoint() string { + if x != nil { + return x.DeviceAuthorizationEndpoint + } + return "" +} + +func (x *DiscoveryResp) GetIntrospectionEndpoint() string { + if x != nil { + return x.IntrospectionEndpoint + } + return "" +} + +func (x *DiscoveryResp) GetGrantTypesSupported() []string { + if x != nil { + return x.GrantTypesSupported + } + return nil +} + +func (x *DiscoveryResp) GetResponseTypesSupported() []string { + if x != nil { + return x.ResponseTypesSupported + } + return nil +} + +func (x *DiscoveryResp) GetSubjectTypesSupported() []string { + if x != nil { + return x.SubjectTypesSupported + } + return nil +} + +func (x *DiscoveryResp) GetIdTokenSigningAlgValuesSupported() []string { + if x != nil { + return x.IdTokenSigningAlgValuesSupported + } + return nil +} + +func (x *DiscoveryResp) GetCodeChallengeMethodsSupported() []string { + if x != nil { + return x.CodeChallengeMethodsSupported + } + return nil +} + +func (x *DiscoveryResp) GetScopesSupported() []string { + if x != nil { + return x.ScopesSupported + } + return nil +} + +func (x *DiscoveryResp) GetTokenEndpointAuthMethodsSupported() []string { + if x != nil { + return x.TokenEndpointAuthMethodsSupported + } + return nil +} +func (x *DiscoveryResp) GetClaimsSupported() []string { + if x != nil { + return x.ClaimsSupported + } + return nil +} + +// RefreshTokenRef contains the metadata for a refresh token that is managed by the storage. +type RefreshTokenRef struct { + state protoimpl.MessageState `protogen:"open.v1"` // ID of the refresh token. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - CreatedAt int64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - LastUsed int64 `protobuf:"varint,6,opt,name=last_used,json=lastUsed,proto3" json:"last_used,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + CreatedAt int64 `protobuf:"varint,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastUsed int64 `protobuf:"varint,6,opt,name=last_used,json=lastUsed,proto3" json:"last_used,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RefreshTokenRef) Reset() { *x = RefreshTokenRef{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RefreshTokenRef) String() string { @@ -1037,8 +1970,8 @@ func (x *RefreshTokenRef) String() string { func (*RefreshTokenRef) ProtoMessage() {} func (x *RefreshTokenRef) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[35] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1050,7 +1983,7 @@ func (x *RefreshTokenRef) ProtoReflect() protoreflect.Message { // Deprecated: Use RefreshTokenRef.ProtoReflect.Descriptor instead. func (*RefreshTokenRef) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{18} + return file_api_v2_api_proto_rawDescGZIP(), []int{35} } func (x *RefreshTokenRef) GetId() string { @@ -1083,21 +2016,18 @@ func (x *RefreshTokenRef) GetLastUsed() int64 { // ListRefreshReq is a request to enumerate the refresh tokens of a user. type ListRefreshReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The "sub" claim returned in the ID Token. - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListRefreshReq) Reset() { *x = ListRefreshReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListRefreshReq) String() string { @@ -1107,8 +2037,8 @@ func (x *ListRefreshReq) String() string { func (*ListRefreshReq) ProtoMessage() {} func (x *ListRefreshReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[36] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1120,7 +2050,7 @@ func (x *ListRefreshReq) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRefreshReq.ProtoReflect.Descriptor instead. func (*ListRefreshReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{19} + return file_api_v2_api_proto_rawDescGZIP(), []int{36} } func (x *ListRefreshReq) GetUserId() string { @@ -1132,20 +2062,17 @@ func (x *ListRefreshReq) GetUserId() string { // ListRefreshResp returns a list of refresh tokens for a user. type ListRefreshResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RefreshTokens []*RefreshTokenRef `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"` unknownFields protoimpl.UnknownFields - - RefreshTokens []*RefreshTokenRef `protobuf:"bytes,1,rep,name=refresh_tokens,json=refreshTokens,proto3" json:"refresh_tokens,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListRefreshResp) Reset() { *x = ListRefreshResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListRefreshResp) String() string { @@ -1155,8 +2082,8 @@ func (x *ListRefreshResp) String() string { func (*ListRefreshResp) ProtoMessage() {} func (x *ListRefreshResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[37] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1168,7 +2095,7 @@ func (x *ListRefreshResp) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRefreshResp.ProtoReflect.Descriptor instead. func (*ListRefreshResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{20} + return file_api_v2_api_proto_rawDescGZIP(), []int{37} } func (x *ListRefreshResp) GetRefreshTokens() []*RefreshTokenRef { @@ -1180,22 +2107,19 @@ func (x *ListRefreshResp) GetRefreshTokens() []*RefreshTokenRef { // RevokeRefreshReq is a request to revoke the refresh token of the user-client pair. type RevokeRefreshReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The "sub" claim returned in the ID Token. - UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RevokeRefreshReq) Reset() { *x = RevokeRefreshReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeRefreshReq) String() string { @@ -1205,8 +2129,8 @@ func (x *RevokeRefreshReq) String() string { func (*RevokeRefreshReq) ProtoMessage() {} func (x *RevokeRefreshReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[38] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1218,7 +2142,7 @@ func (x *RevokeRefreshReq) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeRefreshReq.ProtoReflect.Descriptor instead. func (*RevokeRefreshReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{21} + return file_api_v2_api_proto_rawDescGZIP(), []int{38} } func (x *RevokeRefreshReq) GetUserId() string { @@ -1237,21 +2161,18 @@ func (x *RevokeRefreshReq) GetClientId() string { // RevokeRefreshResp determines if the refresh token is revoked successfully. type RevokeRefreshResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Set to true is refresh token was not found and token could not be revoked. - NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RevokeRefreshResp) Reset() { *x = RevokeRefreshResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeRefreshResp) String() string { @@ -1261,8 +2182,8 @@ func (x *RevokeRefreshResp) String() string { func (*RevokeRefreshResp) ProtoMessage() {} func (x *RevokeRefreshResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[39] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1274,7 +2195,7 @@ func (x *RevokeRefreshResp) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeRefreshResp.ProtoReflect.Descriptor instead. func (*RevokeRefreshResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{22} + return file_api_v2_api_proto_rawDescGZIP(), []int{39} } func (x *RevokeRefreshResp) GetNotFound() bool { @@ -1285,21 +2206,18 @@ func (x *RevokeRefreshResp) GetNotFound() bool { } type VerifyPasswordReq struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` unknownFields protoimpl.UnknownFields - - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` - Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + sizeCache protoimpl.SizeCache } func (x *VerifyPasswordReq) Reset() { *x = VerifyPasswordReq{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VerifyPasswordReq) String() string { @@ -1309,8 +2227,8 @@ func (x *VerifyPasswordReq) String() string { func (*VerifyPasswordReq) ProtoMessage() {} func (x *VerifyPasswordReq) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[40] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1322,7 +2240,7 @@ func (x *VerifyPasswordReq) ProtoReflect() protoreflect.Message { // Deprecated: Use VerifyPasswordReq.ProtoReflect.Descriptor instead. func (*VerifyPasswordReq) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{23} + return file_api_v2_api_proto_rawDescGZIP(), []int{40} } func (x *VerifyPasswordReq) GetEmail() string { @@ -1340,21 +2258,18 @@ func (x *VerifyPasswordReq) GetPassword() string { } type VerifyPasswordResp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"` + NotFound bool `protobuf:"varint,2,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` unknownFields protoimpl.UnknownFields - - Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"` - NotFound bool `protobuf:"varint,2,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` + sizeCache protoimpl.SizeCache } func (x *VerifyPasswordResp) Reset() { *x = VerifyPasswordResp{} - if protoimpl.UnsafeEnabled { - mi := &file_api_v2_api_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_api_v2_api_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *VerifyPasswordResp) String() string { @@ -1364,8 +2279,8 @@ func (x *VerifyPasswordResp) String() string { func (*VerifyPasswordResp) ProtoMessage() {} func (x *VerifyPasswordResp) ProtoReflect() protoreflect.Message { - mi := &file_api_v2_api_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_api_v2_api_proto_msgTypes[41] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1377,7 +2292,7 @@ func (x *VerifyPasswordResp) ProtoReflect() protoreflect.Message { // Deprecated: Use VerifyPasswordResp.ProtoReflect.Descriptor instead. func (*VerifyPasswordResp) Descriptor() ([]byte, []int) { - return file_api_v2_api_proto_rawDescGZIP(), []int{24} + return file_api_v2_api_proto_rawDescGZIP(), []int{41} } func (x *VerifyPasswordResp) GetVerified() bool { @@ -1396,9 +2311,9 @@ func (x *VerifyPasswordResp) GetNotFound() bool { var File_api_v2_api_proto protoreflect.FileDescriptor -var file_api_v2_api_proto_rawDesc = []byte{ +var file_api_v2_api_proto_rawDesc = string([]byte{ 0x0a, 0x10, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0xc1, 0x01, 0x0a, 0x06, 0x43, 0x6c, 0x69, 0x65, + 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0xf0, 0x01, 0x0a, 0x06, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, @@ -1410,231 +2325,422 @@ var file_api_v2_api_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x22, 0x36, 0x0a, 0x0f, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x23, - 0x0a, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x22, 0x5e, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, + 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x12, 0x2d, 0x0a, 0x12, 0x61, + 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0xdc, 0x01, 0x0a, 0x0a, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x69, 0x73, 0x12, 0x23, + 0x0a, 0x0d, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x50, 0x65, + 0x65, 0x72, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x1e, 0x0a, 0x0c, 0x47, 0x65, 0x74, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x34, 0x0a, 0x0d, 0x47, 0x65, 0x74, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x23, 0x0a, 0x06, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, + 0x36, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x12, 0x23, 0x0a, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, 0x5e, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, + 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, + 0x74, 0x73, 0x12, 0x23, 0x0a, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, 0x21, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2f, 0x0a, 0x10, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, + 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xc9, 0x01, 0x0a, 0x0f, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x55, 0x72, 0x69, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, + 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x75, + 0x73, 0x74, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x6c, 0x6f, 0x67, 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6c, 0x6f, 0x67, 0x6f, 0x55, 0x72, 0x6c, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x2f, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, + 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x0f, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x22, 0x3b, 0x0a, 0x0e, 0x4c, 0x69, 0x73, + 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x29, 0x0a, 0x07, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x69, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1a, 0x0a, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x22, 0x3e, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x29, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0d, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x23, - 0x0a, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x22, 0x21, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x2f, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, - 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, - 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x9a, 0x01, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, - 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x69, 0x73, - 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x67, - 0x6f, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, - 0x6f, 0x55, 0x72, 0x6c, 0x22, 0x2f, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, - 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, - 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x69, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, - 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x22, 0x3e, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x29, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, - 0x79, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, - 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x67, 0x0a, - 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, - 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x55, 0x73, - 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x31, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, - 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x29, 0x0a, 0x11, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, - 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x31, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, - 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, - 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x22, 0x3f, 0x0a, 0x10, 0x4c, 0x69, - 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2b, - 0x0a, 0x09, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x52, 0x09, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x22, 0x37, 0x0a, 0x0b, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x12, 0x10, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, - 0x70, 0x69, 0x22, 0x7a, 0x0a, 0x0f, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x52, 0x65, 0x66, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x0d, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x67, + 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, + 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6e, 0x65, 0x77, + 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x55, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x31, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, + 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x29, 0x0a, 0x11, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x31, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, + 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, + 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x11, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x22, 0x3f, 0x0a, 0x10, 0x4c, + 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x12, + 0x2b, 0x0a, 0x09, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x52, 0x09, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x7c, 0x0a, 0x09, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, + 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x42, 0x0a, 0x12, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, + 0x12, 0x2c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x3c, + 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, + 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, + 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x2d, 0x0a, 0x0a, + 0x47, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xb2, 0x01, 0x0a, 0x12, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, + 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x77, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6e, 0x65, + 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x37, 0x0a, 0x0f, 0x6e, 0x65, 0x77, 0x5f, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x73, 0x52, 0x0d, 0x6e, 0x65, 0x77, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, + 0x22, 0x32, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, + 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, + 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x24, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x32, 0x0a, 0x13, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x12, + 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, + 0x65, 0x71, 0x22, 0x43, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x2e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x0c, 0x0a, 0x0a, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x22, 0x37, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, + 0x61, 0x70, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x70, 0x69, 0x22, 0x0e, + 0x0a, 0x0c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x22, 0xb0, + 0x06, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x16, 0x61, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x25, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x77, 0x6b, 0x73, 0x5f, 0x75, + 0x72, 0x69, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x77, 0x6b, 0x73, 0x55, 0x72, + 0x69, 0x12, 0x2b, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x65, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x75, 0x73, + 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x42, + 0x0a, 0x1d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x16, 0x69, 0x6e, 0x74, 0x72, 0x6f, 0x73, 0x70, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x15, 0x69, 0x6e, 0x74, 0x72, 0x6f, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x65, 0x64, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x73, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x38, 0x0a, + 0x18, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x5f, + 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x16, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x53, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x65, 0x64, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, + 0x4f, 0x0a, 0x25, 0x69, 0x64, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x73, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x5f, 0x73, + 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x20, + 0x69, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x6c, + 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, + 0x12, 0x47, 0x0a, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, + 0x72, 0x74, 0x65, 0x64, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x1d, 0x63, 0x6f, 0x64, 0x65, + 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x0d, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x53, 0x75, 0x70, 0x70, 0x6f, + 0x72, 0x74, 0x65, 0x64, 0x12, 0x50, 0x0a, 0x25, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x65, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x0e, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x21, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x53, 0x75, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, + 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x22, 0x7a, 0x0a, 0x0f, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x65, 0x66, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x64, 0x22, 0x29, 0x0a, + 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x3b, 0x0a, 0x0e, 0x72, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, + 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66, 0x52, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, + 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, + 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x73, 0x65, 0x64, 0x22, 0x29, - 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, - 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x0f, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x3b, 0x0a, 0x0e, - 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, - 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x66, 0x52, 0x0d, 0x72, 0x65, 0x66, 0x72, - 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x48, 0x0a, 0x10, 0x52, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x12, 0x17, 0x0a, - 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x49, 0x64, 0x22, 0x30, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, - 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, - 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, - 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x45, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, - 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, - 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4d, 0x0a, 0x12, - 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, - 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0xc7, 0x05, 0x0a, 0x03, - 0x44, 0x65, 0x78, 0x12, 0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, - 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, - 0x12, 0x43, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, - 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, - 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, - 0x3e, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, - 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, - 0x31, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, - 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, - 0x68, 0x12, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, - 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x40, - 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, - 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, - 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, - 0x12, 0x43, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, - 0x65, 0x73, 0x70, 0x22, 0x00, 0x42, 0x36, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x6f, 0x73, 0x2e, 0x64, 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x20, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, 0x70, 0x2f, 0x64, - 0x65, 0x78, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x49, 0x64, 0x22, 0x30, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, + 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, + 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, + 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x45, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, + 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4d, 0x0a, 0x12, 0x56, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x6e, 0x6f, 0x74, 0x5f, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x6e, 0x6f, 0x74, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x32, 0x8b, 0x09, 0x0a, 0x03, 0x44, + 0x65, 0x78, 0x12, 0x34, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, + 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, + 0x43, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3e, + 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, + 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, + 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, + 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x46, + 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, + 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0a, 0x47, 0x65, 0x74, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0c, + 0x47, 0x65, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x12, 0x11, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x1a, + 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x12, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x22, + 0x00, 0x12, 0x40, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, + 0x73, 0x68, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x52, 0x65, 0x73, + 0x70, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, + 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x17, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x42, 0x36, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x6f, 0x73, 0x2e, 0x64, 0x65, 0x78, 0x2e, 0x61, 0x70, 0x69, 0x5a, 0x20, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x78, 0x69, 0x64, + 0x70, 0x2f, 0x64, 0x65, 0x78, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_api_v2_api_proto_rawDescOnce sync.Once - file_api_v2_api_proto_rawDescData = file_api_v2_api_proto_rawDesc + file_api_v2_api_proto_rawDescData []byte ) func file_api_v2_api_proto_rawDescGZIP() []byte { file_api_v2_api_proto_rawDescOnce.Do(func() { - file_api_v2_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_v2_api_proto_rawDescData) + file_api_v2_api_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v2_api_proto_rawDesc), len(file_api_v2_api_proto_rawDesc))) }) return file_api_v2_api_proto_rawDescData } -var file_api_v2_api_proto_msgTypes = make([]protoimpl.MessageInfo, 25) -var file_api_v2_api_proto_goTypes = []interface{}{ - (*Client)(nil), // 0: api.Client - (*CreateClientReq)(nil), // 1: api.CreateClientReq - (*CreateClientResp)(nil), // 2: api.CreateClientResp - (*DeleteClientReq)(nil), // 3: api.DeleteClientReq - (*DeleteClientResp)(nil), // 4: api.DeleteClientResp - (*UpdateClientReq)(nil), // 5: api.UpdateClientReq - (*UpdateClientResp)(nil), // 6: api.UpdateClientResp - (*Password)(nil), // 7: api.Password - (*CreatePasswordReq)(nil), // 8: api.CreatePasswordReq - (*CreatePasswordResp)(nil), // 9: api.CreatePasswordResp - (*UpdatePasswordReq)(nil), // 10: api.UpdatePasswordReq - (*UpdatePasswordResp)(nil), // 11: api.UpdatePasswordResp - (*DeletePasswordReq)(nil), // 12: api.DeletePasswordReq - (*DeletePasswordResp)(nil), // 13: api.DeletePasswordResp - (*ListPasswordReq)(nil), // 14: api.ListPasswordReq - (*ListPasswordResp)(nil), // 15: api.ListPasswordResp - (*VersionReq)(nil), // 16: api.VersionReq - (*VersionResp)(nil), // 17: api.VersionResp - (*RefreshTokenRef)(nil), // 18: api.RefreshTokenRef - (*ListRefreshReq)(nil), // 19: api.ListRefreshReq - (*ListRefreshResp)(nil), // 20: api.ListRefreshResp - (*RevokeRefreshReq)(nil), // 21: api.RevokeRefreshReq - (*RevokeRefreshResp)(nil), // 22: api.RevokeRefreshResp - (*VerifyPasswordReq)(nil), // 23: api.VerifyPasswordReq - (*VerifyPasswordResp)(nil), // 24: api.VerifyPasswordResp +var file_api_v2_api_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_api_v2_api_proto_goTypes = []any{ + (*Client)(nil), // 0: api.Client + (*ClientInfo)(nil), // 1: api.ClientInfo + (*GetClientReq)(nil), // 2: api.GetClientReq + (*GetClientResp)(nil), // 3: api.GetClientResp + (*CreateClientReq)(nil), // 4: api.CreateClientReq + (*CreateClientResp)(nil), // 5: api.CreateClientResp + (*DeleteClientReq)(nil), // 6: api.DeleteClientReq + (*DeleteClientResp)(nil), // 7: api.DeleteClientResp + (*UpdateClientReq)(nil), // 8: api.UpdateClientReq + (*UpdateClientResp)(nil), // 9: api.UpdateClientResp + (*ListClientReq)(nil), // 10: api.ListClientReq + (*ListClientResp)(nil), // 11: api.ListClientResp + (*Password)(nil), // 12: api.Password + (*CreatePasswordReq)(nil), // 13: api.CreatePasswordReq + (*CreatePasswordResp)(nil), // 14: api.CreatePasswordResp + (*UpdatePasswordReq)(nil), // 15: api.UpdatePasswordReq + (*UpdatePasswordResp)(nil), // 16: api.UpdatePasswordResp + (*DeletePasswordReq)(nil), // 17: api.DeletePasswordReq + (*DeletePasswordResp)(nil), // 18: api.DeletePasswordResp + (*ListPasswordReq)(nil), // 19: api.ListPasswordReq + (*ListPasswordResp)(nil), // 20: api.ListPasswordResp + (*Connector)(nil), // 21: api.Connector + (*CreateConnectorReq)(nil), // 22: api.CreateConnectorReq + (*CreateConnectorResp)(nil), // 23: api.CreateConnectorResp + (*GrantTypes)(nil), // 24: api.GrantTypes + (*UpdateConnectorReq)(nil), // 25: api.UpdateConnectorReq + (*UpdateConnectorResp)(nil), // 26: api.UpdateConnectorResp + (*DeleteConnectorReq)(nil), // 27: api.DeleteConnectorReq + (*DeleteConnectorResp)(nil), // 28: api.DeleteConnectorResp + (*ListConnectorReq)(nil), // 29: api.ListConnectorReq + (*ListConnectorResp)(nil), // 30: api.ListConnectorResp + (*VersionReq)(nil), // 31: api.VersionReq + (*VersionResp)(nil), // 32: api.VersionResp + (*DiscoveryReq)(nil), // 33: api.DiscoveryReq + (*DiscoveryResp)(nil), // 34: api.DiscoveryResp + (*RefreshTokenRef)(nil), // 35: api.RefreshTokenRef + (*ListRefreshReq)(nil), // 36: api.ListRefreshReq + (*ListRefreshResp)(nil), // 37: api.ListRefreshResp + (*RevokeRefreshReq)(nil), // 38: api.RevokeRefreshReq + (*RevokeRefreshResp)(nil), // 39: api.RevokeRefreshResp + (*VerifyPasswordReq)(nil), // 40: api.VerifyPasswordReq + (*VerifyPasswordResp)(nil), // 41: api.VerifyPasswordResp } var file_api_v2_api_proto_depIdxs = []int32{ - 0, // 0: api.CreateClientReq.client:type_name -> api.Client - 0, // 1: api.CreateClientResp.client:type_name -> api.Client - 7, // 2: api.CreatePasswordReq.password:type_name -> api.Password - 7, // 3: api.ListPasswordResp.passwords:type_name -> api.Password - 18, // 4: api.ListRefreshResp.refresh_tokens:type_name -> api.RefreshTokenRef - 1, // 5: api.Dex.CreateClient:input_type -> api.CreateClientReq - 5, // 6: api.Dex.UpdateClient:input_type -> api.UpdateClientReq - 3, // 7: api.Dex.DeleteClient:input_type -> api.DeleteClientReq - 8, // 8: api.Dex.CreatePassword:input_type -> api.CreatePasswordReq - 10, // 9: api.Dex.UpdatePassword:input_type -> api.UpdatePasswordReq - 12, // 10: api.Dex.DeletePassword:input_type -> api.DeletePasswordReq - 14, // 11: api.Dex.ListPasswords:input_type -> api.ListPasswordReq - 16, // 12: api.Dex.GetVersion:input_type -> api.VersionReq - 19, // 13: api.Dex.ListRefresh:input_type -> api.ListRefreshReq - 21, // 14: api.Dex.RevokeRefresh:input_type -> api.RevokeRefreshReq - 23, // 15: api.Dex.VerifyPassword:input_type -> api.VerifyPasswordReq - 2, // 16: api.Dex.CreateClient:output_type -> api.CreateClientResp - 6, // 17: api.Dex.UpdateClient:output_type -> api.UpdateClientResp - 4, // 18: api.Dex.DeleteClient:output_type -> api.DeleteClientResp - 9, // 19: api.Dex.CreatePassword:output_type -> api.CreatePasswordResp - 11, // 20: api.Dex.UpdatePassword:output_type -> api.UpdatePasswordResp - 13, // 21: api.Dex.DeletePassword:output_type -> api.DeletePasswordResp - 15, // 22: api.Dex.ListPasswords:output_type -> api.ListPasswordResp - 17, // 23: api.Dex.GetVersion:output_type -> api.VersionResp - 20, // 24: api.Dex.ListRefresh:output_type -> api.ListRefreshResp - 22, // 25: api.Dex.RevokeRefresh:output_type -> api.RevokeRefreshResp - 24, // 26: api.Dex.VerifyPassword:output_type -> api.VerifyPasswordResp - 16, // [16:27] is the sub-list for method output_type - 5, // [5:16] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 0, // 0: api.GetClientResp.client:type_name -> api.Client + 0, // 1: api.CreateClientReq.client:type_name -> api.Client + 0, // 2: api.CreateClientResp.client:type_name -> api.Client + 1, // 3: api.ListClientResp.clients:type_name -> api.ClientInfo + 12, // 4: api.CreatePasswordReq.password:type_name -> api.Password + 12, // 5: api.ListPasswordResp.passwords:type_name -> api.Password + 21, // 6: api.CreateConnectorReq.connector:type_name -> api.Connector + 24, // 7: api.UpdateConnectorReq.new_grant_types:type_name -> api.GrantTypes + 21, // 8: api.ListConnectorResp.connectors:type_name -> api.Connector + 35, // 9: api.ListRefreshResp.refresh_tokens:type_name -> api.RefreshTokenRef + 2, // 10: api.Dex.GetClient:input_type -> api.GetClientReq + 4, // 11: api.Dex.CreateClient:input_type -> api.CreateClientReq + 8, // 12: api.Dex.UpdateClient:input_type -> api.UpdateClientReq + 6, // 13: api.Dex.DeleteClient:input_type -> api.DeleteClientReq + 10, // 14: api.Dex.ListClients:input_type -> api.ListClientReq + 13, // 15: api.Dex.CreatePassword:input_type -> api.CreatePasswordReq + 15, // 16: api.Dex.UpdatePassword:input_type -> api.UpdatePasswordReq + 17, // 17: api.Dex.DeletePassword:input_type -> api.DeletePasswordReq + 19, // 18: api.Dex.ListPasswords:input_type -> api.ListPasswordReq + 22, // 19: api.Dex.CreateConnector:input_type -> api.CreateConnectorReq + 25, // 20: api.Dex.UpdateConnector:input_type -> api.UpdateConnectorReq + 27, // 21: api.Dex.DeleteConnector:input_type -> api.DeleteConnectorReq + 29, // 22: api.Dex.ListConnectors:input_type -> api.ListConnectorReq + 31, // 23: api.Dex.GetVersion:input_type -> api.VersionReq + 33, // 24: api.Dex.GetDiscovery:input_type -> api.DiscoveryReq + 36, // 25: api.Dex.ListRefresh:input_type -> api.ListRefreshReq + 38, // 26: api.Dex.RevokeRefresh:input_type -> api.RevokeRefreshReq + 40, // 27: api.Dex.VerifyPassword:input_type -> api.VerifyPasswordReq + 3, // 28: api.Dex.GetClient:output_type -> api.GetClientResp + 5, // 29: api.Dex.CreateClient:output_type -> api.CreateClientResp + 9, // 30: api.Dex.UpdateClient:output_type -> api.UpdateClientResp + 7, // 31: api.Dex.DeleteClient:output_type -> api.DeleteClientResp + 11, // 32: api.Dex.ListClients:output_type -> api.ListClientResp + 14, // 33: api.Dex.CreatePassword:output_type -> api.CreatePasswordResp + 16, // 34: api.Dex.UpdatePassword:output_type -> api.UpdatePasswordResp + 18, // 35: api.Dex.DeletePassword:output_type -> api.DeletePasswordResp + 20, // 36: api.Dex.ListPasswords:output_type -> api.ListPasswordResp + 23, // 37: api.Dex.CreateConnector:output_type -> api.CreateConnectorResp + 26, // 38: api.Dex.UpdateConnector:output_type -> api.UpdateConnectorResp + 28, // 39: api.Dex.DeleteConnector:output_type -> api.DeleteConnectorResp + 30, // 40: api.Dex.ListConnectors:output_type -> api.ListConnectorResp + 32, // 41: api.Dex.GetVersion:output_type -> api.VersionResp + 34, // 42: api.Dex.GetDiscovery:output_type -> api.DiscoveryResp + 37, // 43: api.Dex.ListRefresh:output_type -> api.ListRefreshResp + 39, // 44: api.Dex.RevokeRefresh:output_type -> api.RevokeRefreshResp + 41, // 45: api.Dex.VerifyPassword:output_type -> api.VerifyPasswordResp + 28, // [28:46] is the sub-list for method output_type + 10, // [10:28] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_api_v2_api_proto_init() } @@ -1642,315 +2748,13 @@ func file_api_v2_api_proto_init() { if File_api_v2_api_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_api_v2_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Client); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateClientReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateClientResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteClientReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteClientResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateClientReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateClientResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Password); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreatePasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreatePasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdatePasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeletePasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListPasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VersionReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VersionResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RefreshTokenRef); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRefreshReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListRefreshResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeRefreshReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeRefreshResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VerifyPasswordReq); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_v2_api_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VerifyPasswordResp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_api_v2_api_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v2_api_proto_rawDesc), len(file_api_v2_api_proto_rawDesc)), NumEnums: 0, - NumMessages: 25, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, @@ -1959,7 +2763,6 @@ func file_api_v2_api_proto_init() { MessageInfos: file_api_v2_api_proto_msgTypes, }.Build() File_api_v2_api_proto = out.File - file_api_v2_api_proto_rawDesc = nil file_api_v2_api_proto_goTypes = nil file_api_v2_api_proto_depIdxs = nil } diff --git a/api/v2/api.proto b/api/v2/api.proto index 82a2e2afa1..4d5b850e79 100644 --- a/api/v2/api.proto +++ b/api/v2/api.proto @@ -14,6 +14,29 @@ message Client { bool public = 5; string name = 6; string logo_url = 7; + repeated string allowed_connectors = 8; +} + +// ClientInfo represents an OAuth2 client without sensitive information. +message ClientInfo { + string id = 1; + repeated string redirect_uris = 2; + repeated string trusted_peers = 3; + bool public = 4; + string name = 5; + string logo_url = 6; + repeated string allowed_connectors = 7; +} + +// GetClientReq is a request to retrieve client details. +message GetClientReq { + // The ID of the client. + string id = 1; +} + +// GetClientResp returns the client details. +message GetClientResp { + Client client = 1; } // CreateClientReq is a request to make a client. @@ -45,6 +68,7 @@ message UpdateClientReq { repeated string trusted_peers = 3; string name = 4; string logo_url = 5; + repeated string allowed_connectors = 6; } // UpdateClientResp returns the response from updating a client. @@ -52,6 +76,14 @@ message UpdateClientResp { bool not_found = 1; } +// ListClientReq is a request to enumerate clients. +message ListClientReq {} + +// ListClientResp returns a list of clients. +message ListClientResp { + repeated ClientInfo clients = 1; +} + // TODO(ericchiang): expand this. // Password is an email for password mapping managed by the storage. @@ -105,6 +137,67 @@ message ListPasswordResp { repeated Password passwords = 1; } +// Connector is a strategy used by Dex for authenticating a user against another identity provider +message Connector { + string id = 1; + string type = 2; + string name = 3; + bytes config = 4; + repeated string grant_types = 5; +} + +// CreateConnectorReq is a request to make a connector. +message CreateConnectorReq { + Connector connector = 1; +} + +// CreateConnectorResp returns the response from creating a connector. +message CreateConnectorResp { + bool already_exists = 1; +} + +// GrantTypes wraps a list of grant types to distinguish between +// "not specified" (no update) and "empty list" (unrestricted). +message GrantTypes { + repeated string grant_types = 1; +} + +// UpdateConnectorReq is a request to modify an existing connector. +message UpdateConnectorReq { + // The id used to lookup the connector. This field cannot be modified + string id = 1; + string new_type = 2; + string new_name = 3; + bytes new_config = 4; + // If set, updates the connector's allowed grant types. + // An empty grant_types list means unrestricted (all grant types allowed). + // If not set (null), grant types are not modified. + GrantTypes new_grant_types = 5; +} + +// UpdateConnectorResp returns the response from modifying an existing connector. +message UpdateConnectorResp { + bool not_found = 1; +} + +// DeleteConnectorReq is a request to delete a connector. +message DeleteConnectorReq { + string id = 1; +} + +// DeleteConnectorResp returns the response from deleting a connector. +message DeleteConnectorResp { + bool not_found = 1; +} + +// ListConnectorReq is a request to enumerate connectors. +message ListConnectorReq {} + +// ListConnectorResp returns a list of connectors. +message ListConnectorResp { + repeated Connector connectors = 1; +} + // VersionReq is a request to fetch version info. message VersionReq {} @@ -112,11 +205,33 @@ message VersionReq {} message VersionResp { // Semantic version of the server. string server = 1; - // Numeric version of the API. It increases everytime a new call is added to the API. + // Numeric version of the API. It increases every time a new call is added to the API. // Clients should use this info to determine if the server supports specific features. int32 api = 2; } +// DiscoveryReq is a request to fetch discover information. +message DiscoveryReq {} + +//DiscoverResp holds the version oidc disovery info. +message DiscoveryResp { + string issuer = 1; + string authorization_endpoint = 2; + string token_endpoint = 3; + string jwks_uri = 4; + string userinfo_endpoint = 5; + string device_authorization_endpoint = 6; + string introspection_endpoint = 7; + repeated string grant_types_supported = 8; + repeated string response_types_supported = 9; + repeated string subject_types_supported = 10; + repeated string id_token_signing_alg_values_supported = 11; + repeated string code_challenge_methods_supported = 12; + repeated string scopes_supported = 13; + repeated string token_endpoint_auth_methods_supported = 14; + repeated string claims_supported = 15; +} + // RefreshTokenRef contains the metadata for a refresh token that is managed by the storage. message RefreshTokenRef { // ID of the refresh token. @@ -162,12 +277,16 @@ message VerifyPasswordResp { // Dex represents the dex gRPC service. service Dex { + // GetClient gets a client. + rpc GetClient(GetClientReq) returns (GetClientResp) {}; // CreateClient creates a client. rpc CreateClient(CreateClientReq) returns (CreateClientResp) {}; // UpdateClient updates an existing client rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {}; // DeleteClient deletes the provided client. rpc DeleteClient(DeleteClientReq) returns (DeleteClientResp) {}; + // ListClients lists all client entries. + rpc ListClients(ListClientReq) returns (ListClientResp) {}; // CreatePassword creates a password. rpc CreatePassword(CreatePasswordReq) returns (CreatePasswordResp) {}; // UpdatePassword modifies existing password. @@ -176,8 +295,18 @@ service Dex { rpc DeletePassword(DeletePasswordReq) returns (DeletePasswordResp) {}; // ListPassword lists all password entries. rpc ListPasswords(ListPasswordReq) returns (ListPasswordResp) {}; + // CreateConnector creates a connector. + rpc CreateConnector(CreateConnectorReq) returns (CreateConnectorResp) {}; + // UpdateConnector modifies existing connector. + rpc UpdateConnector(UpdateConnectorReq) returns (UpdateConnectorResp) {}; + // DeleteConnector deletes the connector. + rpc DeleteConnector(DeleteConnectorReq) returns (DeleteConnectorResp) {}; + // ListConnectors lists all connector entries. + rpc ListConnectors(ListConnectorReq) returns (ListConnectorResp) {}; // GetVersion returns version information of the server. rpc GetVersion(VersionReq) returns (VersionResp) {}; + // GetDiscovery returns discovery information of the server. + rpc GetDiscovery(DiscoveryReq) returns (DiscoveryResp) {}; // ListRefresh lists all the refresh token entries for a particular user. rpc ListRefresh(ListRefreshReq) returns (ListRefreshResp) {}; // RevokeRefresh revokes the refresh token for the provided user-client pair. diff --git a/api/v2/api_grpc.pb.go b/api/v2/api_grpc.pb.go index 8b3b10bc52..3fe210e6ff 100644 --- a/api/v2/api_grpc.pb.go +++ b/api/v2/api_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: api/v2/api.proto package api @@ -11,19 +15,46 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Dex_GetClient_FullMethodName = "/api.Dex/GetClient" + Dex_CreateClient_FullMethodName = "/api.Dex/CreateClient" + Dex_UpdateClient_FullMethodName = "/api.Dex/UpdateClient" + Dex_DeleteClient_FullMethodName = "/api.Dex/DeleteClient" + Dex_ListClients_FullMethodName = "/api.Dex/ListClients" + Dex_CreatePassword_FullMethodName = "/api.Dex/CreatePassword" + Dex_UpdatePassword_FullMethodName = "/api.Dex/UpdatePassword" + Dex_DeletePassword_FullMethodName = "/api.Dex/DeletePassword" + Dex_ListPasswords_FullMethodName = "/api.Dex/ListPasswords" + Dex_CreateConnector_FullMethodName = "/api.Dex/CreateConnector" + Dex_UpdateConnector_FullMethodName = "/api.Dex/UpdateConnector" + Dex_DeleteConnector_FullMethodName = "/api.Dex/DeleteConnector" + Dex_ListConnectors_FullMethodName = "/api.Dex/ListConnectors" + Dex_GetVersion_FullMethodName = "/api.Dex/GetVersion" + Dex_GetDiscovery_FullMethodName = "/api.Dex/GetDiscovery" + Dex_ListRefresh_FullMethodName = "/api.Dex/ListRefresh" + Dex_RevokeRefresh_FullMethodName = "/api.Dex/RevokeRefresh" + Dex_VerifyPassword_FullMethodName = "/api.Dex/VerifyPassword" +) // DexClient is the client API for Dex service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Dex represents the dex gRPC service. type DexClient interface { + // GetClient gets a client. + GetClient(ctx context.Context, in *GetClientReq, opts ...grpc.CallOption) (*GetClientResp, error) // CreateClient creates a client. CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error) // UpdateClient updates an existing client UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error) // DeleteClient deletes the provided client. DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error) + // ListClients lists all client entries. + ListClients(ctx context.Context, in *ListClientReq, opts ...grpc.CallOption) (*ListClientResp, error) // CreatePassword creates a password. CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) // UpdatePassword modifies existing password. @@ -32,8 +63,18 @@ type DexClient interface { DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error) // ListPassword lists all password entries. ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error) + // CreateConnector creates a connector. + CreateConnector(ctx context.Context, in *CreateConnectorReq, opts ...grpc.CallOption) (*CreateConnectorResp, error) + // UpdateConnector modifies existing connector. + UpdateConnector(ctx context.Context, in *UpdateConnectorReq, opts ...grpc.CallOption) (*UpdateConnectorResp, error) + // DeleteConnector deletes the connector. + DeleteConnector(ctx context.Context, in *DeleteConnectorReq, opts ...grpc.CallOption) (*DeleteConnectorResp, error) + // ListConnectors lists all connector entries. + ListConnectors(ctx context.Context, in *ListConnectorReq, opts ...grpc.CallOption) (*ListConnectorResp, error) // GetVersion returns version information of the server. GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) + // GetDiscovery returns discovery information of the server. + GetDiscovery(ctx context.Context, in *DiscoveryReq, opts ...grpc.CallOption) (*DiscoveryResp, error) // ListRefresh lists all the refresh token entries for a particular user. ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error) // RevokeRefresh revokes the refresh token for the provided user-client pair. @@ -52,9 +93,20 @@ func NewDexClient(cc grpc.ClientConnInterface) DexClient { return &dexClient{cc} } +func (c *dexClient) GetClient(ctx context.Context, in *GetClientReq, opts ...grpc.CallOption) (*GetClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetClientResp) + err := c.cc.Invoke(ctx, Dex_GetClient_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *dexClient) CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreateClientResp) - err := c.cc.Invoke(ctx, "/api.Dex/CreateClient", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_CreateClient_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -62,8 +114,9 @@ func (c *dexClient) CreateClient(ctx context.Context, in *CreateClientReq, opts } func (c *dexClient) UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UpdateClientResp) - err := c.cc.Invoke(ctx, "/api.Dex/UpdateClient", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_UpdateClient_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -71,8 +124,19 @@ func (c *dexClient) UpdateClient(ctx context.Context, in *UpdateClientReq, opts } func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteClientResp) - err := c.cc.Invoke(ctx, "/api.Dex/DeleteClient", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_DeleteClient_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) ListClients(ctx context.Context, in *ListClientReq, opts ...grpc.CallOption) (*ListClientResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListClientResp) + err := c.cc.Invoke(ctx, Dex_ListClients_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -80,8 +144,9 @@ func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts } func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CreatePasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/CreatePassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_CreatePassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -89,8 +154,9 @@ func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, o } func (c *dexClient) UpdatePassword(ctx context.Context, in *UpdatePasswordReq, opts ...grpc.CallOption) (*UpdatePasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(UpdatePasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/UpdatePassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_UpdatePassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -98,8 +164,9 @@ func (c *dexClient) UpdatePassword(ctx context.Context, in *UpdatePasswordReq, o } func (c *dexClient) DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeletePasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/DeletePassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_DeletePassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -107,8 +174,49 @@ func (c *dexClient) DeletePassword(ctx context.Context, in *DeletePasswordReq, o } func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListPasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/ListPasswords", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_ListPasswords_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) CreateConnector(ctx context.Context, in *CreateConnectorReq, opts ...grpc.CallOption) (*CreateConnectorResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateConnectorResp) + err := c.cc.Invoke(ctx, Dex_CreateConnector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) UpdateConnector(ctx context.Context, in *UpdateConnectorReq, opts ...grpc.CallOption) (*UpdateConnectorResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateConnectorResp) + err := c.cc.Invoke(ctx, Dex_UpdateConnector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) DeleteConnector(ctx context.Context, in *DeleteConnectorReq, opts ...grpc.CallOption) (*DeleteConnectorResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteConnectorResp) + err := c.cc.Invoke(ctx, Dex_DeleteConnector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) ListConnectors(ctx context.Context, in *ListConnectorReq, opts ...grpc.CallOption) (*ListConnectorResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListConnectorResp) + err := c.cc.Invoke(ctx, Dex_ListConnectors_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -116,8 +224,19 @@ func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts } func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(VersionResp) - err := c.cc.Invoke(ctx, "/api.Dex/GetVersion", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_GetVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dexClient) GetDiscovery(ctx context.Context, in *DiscoveryReq, opts ...grpc.CallOption) (*DiscoveryResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DiscoveryResp) + err := c.cc.Invoke(ctx, Dex_GetDiscovery_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -125,8 +244,9 @@ func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc } func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListRefreshResp) - err := c.cc.Invoke(ctx, "/api.Dex/ListRefresh", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_ListRefresh_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -134,8 +254,9 @@ func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts .. } func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RevokeRefreshResp) - err := c.cc.Invoke(ctx, "/api.Dex/RevokeRefresh", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_RevokeRefresh_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -143,8 +264,9 @@ func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opt } func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(VerifyPasswordResp) - err := c.cc.Invoke(ctx, "/api.Dex/VerifyPassword", in, out, opts...) + err := c.cc.Invoke(ctx, Dex_VerifyPassword_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -153,14 +275,20 @@ func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, o // DexServer is the server API for Dex service. // All implementations must embed UnimplementedDexServer -// for forward compatibility +// for forward compatibility. +// +// Dex represents the dex gRPC service. type DexServer interface { + // GetClient gets a client. + GetClient(context.Context, *GetClientReq) (*GetClientResp, error) // CreateClient creates a client. CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error) // UpdateClient updates an existing client UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error) // DeleteClient deletes the provided client. DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) + // ListClients lists all client entries. + ListClients(context.Context, *ListClientReq) (*ListClientResp, error) // CreatePassword creates a password. CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) // UpdatePassword modifies existing password. @@ -169,8 +297,18 @@ type DexServer interface { DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error) // ListPassword lists all password entries. ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error) + // CreateConnector creates a connector. + CreateConnector(context.Context, *CreateConnectorReq) (*CreateConnectorResp, error) + // UpdateConnector modifies existing connector. + UpdateConnector(context.Context, *UpdateConnectorReq) (*UpdateConnectorResp, error) + // DeleteConnector deletes the connector. + DeleteConnector(context.Context, *DeleteConnectorReq) (*DeleteConnectorResp, error) + // ListConnectors lists all connector entries. + ListConnectors(context.Context, *ListConnectorReq) (*ListConnectorResp, error) // GetVersion returns version information of the server. GetVersion(context.Context, *VersionReq) (*VersionResp, error) + // GetDiscovery returns discovery information of the server. + GetDiscovery(context.Context, *DiscoveryReq) (*DiscoveryResp, error) // ListRefresh lists all the refresh token entries for a particular user. ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error) // RevokeRefresh revokes the refresh token for the provided user-client pair. @@ -182,10 +320,16 @@ type DexServer interface { mustEmbedUnimplementedDexServer() } -// UnimplementedDexServer must be embedded to have forward compatible implementations. -type UnimplementedDexServer struct { -} +// UnimplementedDexServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDexServer struct{} +func (UnimplementedDexServer) GetClient(context.Context, *GetClientReq) (*GetClientResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetClient not implemented") +} func (UnimplementedDexServer) CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateClient not implemented") } @@ -195,6 +339,9 @@ func (UnimplementedDexServer) UpdateClient(context.Context, *UpdateClientReq) (* func (UnimplementedDexServer) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteClient not implemented") } +func (UnimplementedDexServer) ListClients(context.Context, *ListClientReq) (*ListClientResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListClients not implemented") +} func (UnimplementedDexServer) CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) { return nil, status.Errorf(codes.Unimplemented, "method CreatePassword not implemented") } @@ -207,9 +354,24 @@ func (UnimplementedDexServer) DeletePassword(context.Context, *DeletePasswordReq func (UnimplementedDexServer) ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error) { return nil, status.Errorf(codes.Unimplemented, "method ListPasswords not implemented") } +func (UnimplementedDexServer) CreateConnector(context.Context, *CreateConnectorReq) (*CreateConnectorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateConnector not implemented") +} +func (UnimplementedDexServer) UpdateConnector(context.Context, *UpdateConnectorReq) (*UpdateConnectorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateConnector not implemented") +} +func (UnimplementedDexServer) DeleteConnector(context.Context, *DeleteConnectorReq) (*DeleteConnectorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteConnector not implemented") +} +func (UnimplementedDexServer) ListConnectors(context.Context, *ListConnectorReq) (*ListConnectorResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListConnectors not implemented") +} func (UnimplementedDexServer) GetVersion(context.Context, *VersionReq) (*VersionResp, error) { return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented") } +func (UnimplementedDexServer) GetDiscovery(context.Context, *DiscoveryReq) (*DiscoveryResp, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetDiscovery not implemented") +} func (UnimplementedDexServer) ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error) { return nil, status.Errorf(codes.Unimplemented, "method ListRefresh not implemented") } @@ -220,6 +382,7 @@ func (UnimplementedDexServer) VerifyPassword(context.Context, *VerifyPasswordReq return nil, status.Errorf(codes.Unimplemented, "method VerifyPassword not implemented") } func (UnimplementedDexServer) mustEmbedUnimplementedDexServer() {} +func (UnimplementedDexServer) testEmbeddedByValue() {} // UnsafeDexServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to DexServer will @@ -229,9 +392,34 @@ type UnsafeDexServer interface { } func RegisterDexServer(s grpc.ServiceRegistrar, srv DexServer) { + // If the following call pancis, it indicates UnimplementedDexServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&Dex_ServiceDesc, srv) } +func _Dex_GetClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetClientReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).GetClient(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_GetClient_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).GetClient(ctx, req.(*GetClientReq)) + } + return interceptor(ctx, in, info, handler) +} + func _Dex_CreateClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateClientReq) if err := dec(in); err != nil { @@ -242,7 +430,7 @@ func _Dex_CreateClient_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/CreateClient", + FullMethod: Dex_CreateClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).CreateClient(ctx, req.(*CreateClientReq)) @@ -260,7 +448,7 @@ func _Dex_UpdateClient_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/UpdateClient", + FullMethod: Dex_UpdateClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).UpdateClient(ctx, req.(*UpdateClientReq)) @@ -278,7 +466,7 @@ func _Dex_DeleteClient_Handler(srv interface{}, ctx context.Context, dec func(in } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/DeleteClient", + FullMethod: Dex_DeleteClient_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).DeleteClient(ctx, req.(*DeleteClientReq)) @@ -286,6 +474,24 @@ func _Dex_DeleteClient_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +func _Dex_ListClients_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListClientReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).ListClients(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_ListClients_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).ListClients(ctx, req.(*ListClientReq)) + } + return interceptor(ctx, in, info, handler) +} + func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreatePasswordReq) if err := dec(in); err != nil { @@ -296,7 +502,7 @@ func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/CreatePassword", + FullMethod: Dex_CreatePassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).CreatePassword(ctx, req.(*CreatePasswordReq)) @@ -314,7 +520,7 @@ func _Dex_UpdatePassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/UpdatePassword", + FullMethod: Dex_UpdatePassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).UpdatePassword(ctx, req.(*UpdatePasswordReq)) @@ -332,7 +538,7 @@ func _Dex_DeletePassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/DeletePassword", + FullMethod: Dex_DeletePassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).DeletePassword(ctx, req.(*DeletePasswordReq)) @@ -350,7 +556,7 @@ func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/ListPasswords", + FullMethod: Dex_ListPasswords_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).ListPasswords(ctx, req.(*ListPasswordReq)) @@ -358,6 +564,78 @@ func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(i return interceptor(ctx, in, info, handler) } +func _Dex_CreateConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateConnectorReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).CreateConnector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_CreateConnector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).CreateConnector(ctx, req.(*CreateConnectorReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_UpdateConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateConnectorReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).UpdateConnector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_UpdateConnector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).UpdateConnector(ctx, req.(*UpdateConnectorReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_DeleteConnector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteConnectorReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).DeleteConnector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_DeleteConnector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).DeleteConnector(ctx, req.(*DeleteConnectorReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dex_ListConnectors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListConnectorReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).ListConnectors(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_ListConnectors_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).ListConnectors(ctx, req.(*ListConnectorReq)) + } + return interceptor(ctx, in, info, handler) +} + func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(VersionReq) if err := dec(in); err != nil { @@ -368,7 +646,7 @@ func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(inte } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/GetVersion", + FullMethod: Dex_GetVersion_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).GetVersion(ctx, req.(*VersionReq)) @@ -376,6 +654,24 @@ func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(inte return interceptor(ctx, in, info, handler) } +func _Dex_GetDiscovery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DiscoveryReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DexServer).GetDiscovery(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Dex_GetDiscovery_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DexServer).GetDiscovery(ctx, req.(*DiscoveryReq)) + } + return interceptor(ctx, in, info, handler) +} + func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListRefreshReq) if err := dec(in); err != nil { @@ -386,7 +682,7 @@ func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/ListRefresh", + FullMethod: Dex_ListRefresh_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).ListRefresh(ctx, req.(*ListRefreshReq)) @@ -404,7 +700,7 @@ func _Dex_RevokeRefresh_Handler(srv interface{}, ctx context.Context, dec func(i } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/RevokeRefresh", + FullMethod: Dex_RevokeRefresh_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).RevokeRefresh(ctx, req.(*RevokeRefreshReq)) @@ -422,7 +718,7 @@ func _Dex_VerifyPassword_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/api.Dex/VerifyPassword", + FullMethod: Dex_VerifyPassword_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(DexServer).VerifyPassword(ctx, req.(*VerifyPasswordReq)) @@ -437,6 +733,10 @@ var Dex_ServiceDesc = grpc.ServiceDesc{ ServiceName: "api.Dex", HandlerType: (*DexServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "GetClient", + Handler: _Dex_GetClient_Handler, + }, { MethodName: "CreateClient", Handler: _Dex_CreateClient_Handler, @@ -449,6 +749,10 @@ var Dex_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteClient", Handler: _Dex_DeleteClient_Handler, }, + { + MethodName: "ListClients", + Handler: _Dex_ListClients_Handler, + }, { MethodName: "CreatePassword", Handler: _Dex_CreatePassword_Handler, @@ -465,10 +769,30 @@ var Dex_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListPasswords", Handler: _Dex_ListPasswords_Handler, }, + { + MethodName: "CreateConnector", + Handler: _Dex_CreateConnector_Handler, + }, + { + MethodName: "UpdateConnector", + Handler: _Dex_UpdateConnector_Handler, + }, + { + MethodName: "DeleteConnector", + Handler: _Dex_DeleteConnector_Handler, + }, + { + MethodName: "ListConnectors", + Handler: _Dex_ListConnectors_Handler, + }, { MethodName: "GetVersion", Handler: _Dex_GetVersion_Handler, }, + { + MethodName: "GetDiscovery", + Handler: _Dex_GetDiscovery_Handler, + }, { MethodName: "ListRefresh", Handler: _Dex_ListRefresh_Handler, diff --git a/api/v2/go.mod b/api/v2/go.mod index dc78ec4d96..289e3b81d7 100644 --- a/api/v2/go.mod +++ b/api/v2/go.mod @@ -1,16 +1,15 @@ module github.com/dexidp/dex/api/v2 -go 1.17 +go 1.24.0 require ( - google.golang.org/grpc v1.47.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/grpc v1.79.2 + google.golang.org/protobuf v1.36.11 ) require ( - github.com/golang/protobuf v1.5.2 // indirect - golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect ) diff --git a/api/v2/go.sum b/api/v2/go.sum index 59d53d2d63..e9906532fe 100644 --- a/api/v2/go.sum +++ b/api/v2/go.sum @@ -1,142 +1,38 @@ -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= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -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-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM= -google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -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.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 7bb7fbb780..48f2c05652 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -1,17 +1,21 @@ package main import ( + "bytes" "encoding/base64" "encoding/json" "fmt" + "log/slog" + "net/http" + "net/netip" "os" - "strconv" "strings" "golang.org/x/crypto/bcrypt" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/featureflags" "github.com/dexidp/dex/server" + "github.com/dexidp/dex/server/signer" "github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage/ent" "github.com/dexidp/dex/storage/etcd" @@ -20,6 +24,15 @@ import ( "github.com/dexidp/dex/storage/sql" ) +func configUnmarshaller(b []byte, v interface{}) error { + if !featureflags.ConfigDisallowUnknownFields.Enabled() { + return json.Unmarshal(b, v) + } + dec := json.NewDecoder(bytes.NewReader(b)) + dec.DisallowUnknownFields() + return dec.Decode(v) +} + // Config is the config format for the main application. type Config struct { Issuer string `json:"issuer"` @@ -33,6 +46,9 @@ type Config struct { Frontend server.WebConfig `json:"frontend"` + // Signer configuration controls signing of JWT tokens issued by Dex. + Signer Signer `json:"signer"` + // StaticConnectors are user defined connectors specified in the ConfigMap // Write operations, like updating a connector, will fail. StaticConnectors []Connector `json:"connectors"` @@ -49,6 +65,23 @@ type Config struct { // querying the storage. Cannot be specified without enabling a passwords // database. StaticPasswords []password `json:"staticPasswords"` + + // Sessions holds authentication session configuration. + // Requires DEX_SESSIONS_ENABLED=true feature flag. + Sessions *Sessions `json:"sessions"` + + // MFA holds multi-factor authentication configuration. + MFA MFAConfig `json:"mfa"` +} + +// MFAConfig holds multi-factor authentication settings. +type MFAConfig struct { + // Authenticators defines MFA providers available for clients to reference. + Authenticators []MFAAuthenticator `json:"authenticators"` + + // DefaultMFAChain is the default ordered list of authenticator IDs applied + // to clients that don't specify their own mfaChain. Empty means no MFA by default. + DefaultMFAChain []string `json:"defaultMFAChain"` } // Validate the configuration @@ -64,10 +97,16 @@ func (c Config) Validate() error { {c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"}, {c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"}, {c.Web.HTTPS != "" && c.Web.TLSKey == "", "no private key specified for HTTPS"}, + {c.Web.TLSMinVersion != "" && c.Web.TLSMinVersion != "1.2" && c.Web.TLSMinVersion != "1.3", "supported TLS versions are: 1.2, 1.3"}, + {c.Web.TLSMaxVersion != "" && c.Web.TLSMaxVersion != "1.2" && c.Web.TLSMaxVersion != "1.3", "supported TLS versions are: 1.2, 1.3"}, + {c.Web.TLSMaxVersion != "" && c.Web.TLSMinVersion != "" && c.Web.TLSMinVersion > c.Web.TLSMaxVersion, "TLSMinVersion greater than TLSMaxVersion"}, {c.GRPC.TLSCert != "" && c.GRPC.Addr == "", "no address specified for gRPC"}, {c.GRPC.TLSKey != "" && c.GRPC.Addr == "", "no address specified for gRPC"}, {(c.GRPC.TLSCert == "") != (c.GRPC.TLSKey == ""), "must specific both a gRPC TLS cert and key"}, {c.GRPC.TLSCert == "" && c.GRPC.TLSClientCA != "", "cannot specify gRPC TLS client CA without a gRPC TLS cert"}, + {c.GRPC.TLSMinVersion != "" && c.GRPC.TLSMinVersion != "1.2" && c.GRPC.TLSMinVersion != "1.3", "supported TLS versions are: 1.2, 1.3"}, + {c.GRPC.TLSMaxVersion != "" && c.GRPC.TLSMaxVersion != "1.2" && c.GRPC.TLSMaxVersion != "1.3", "supported TLS versions are: 1.2, 1.3"}, + {c.GRPC.TLSMaxVersion != "" && c.GRPC.TLSMinVersion != "" && c.GRPC.TLSMinVersion > c.GRPC.TLSMaxVersion, "TLSMinVersion greater than TLSMaxVersion"}, } var checkErrors []string @@ -77,9 +116,63 @@ func (c Config) Validate() error { checkErrors = append(checkErrors, check.errMsg) } } + if len(checkErrors) != 0 { return fmt.Errorf("invalid Config:\n\t-\t%s", strings.Join(checkErrors, "\n\t-\t")) } + + if c.Sessions != nil && !featureflags.SessionsEnabled.Enabled() { + return fmt.Errorf("sessions config requires sessions to be enabled (DEX_SESSIONS_ENABLED=true)") + } + + if err := c.validateMFA(); err != nil { + return err + } + + return nil +} + +func (c Config) validateMFA() error { + mfa := c.MFA + if len(mfa.Authenticators) == 0 && len(mfa.DefaultMFAChain) == 0 { + return nil + } + + if !featureflags.SessionsEnabled.Enabled() { + return fmt.Errorf("mfa requires sessions to be enabled (DEX_SESSIONS_ENABLED=true)") + } + + knownTypes := map[string]bool{"TOTP": true} + ids := make(map[string]bool, len(mfa.Authenticators)) + + for _, auth := range mfa.Authenticators { + if auth.ID == "" { + return fmt.Errorf("mfa.authenticators: authenticator must have an id") + } + if ids[auth.ID] { + return fmt.Errorf("mfa.authenticators: duplicate authenticator id %q", auth.ID) + } + ids[auth.ID] = true + + if !knownTypes[auth.Type] { + return fmt.Errorf("mfa.authenticators: unknown type %q for authenticator %q", auth.Type, auth.ID) + } + } + + for _, authID := range mfa.DefaultMFAChain { + if !ids[authID] { + return fmt.Errorf("mfa.defaultMFAChain: references unknown authenticator %q", authID) + } + } + + for _, client := range c.StaticClients { + for _, authID := range client.MFAChain { + if !ids[authID] { + return fmt.Errorf("staticClients: client %q references unknown MFA authenticator %q", client.ID, authID) + } + } + } + return nil } @@ -87,19 +180,27 @@ type password storage.Password func (p *password) UnmarshalJSON(b []byte) error { var data struct { - Email string `json:"email"` - Username string `json:"username"` - UserID string `json:"userID"` - Hash string `json:"hash"` - HashFromEnv string `json:"hashFromEnv"` + Email string `json:"email"` + Username string `json:"username"` + Name string `json:"name"` + PreferredUsername string `json:"preferredUsername"` + EmailVerified *bool `json:"emailVerified"` + UserID string `json:"userID"` + Hash string `json:"hash"` + HashFromEnv string `json:"hashFromEnv"` + Groups []string `json:"groups"` } - if err := json.Unmarshal(b, &data); err != nil { + if err := configUnmarshaller(b, &data); err != nil { return err } *p = password(storage.Password{ - Email: data.Email, - Username: data.Username, - UserID: data.UserID, + Email: data.Email, + Username: data.Username, + Name: data.Name, + PreferredUsername: data.PreferredUsername, + EmailVerified: data.EmailVerified, + UserID: data.UserID, + Groups: data.Groups, }) if len(data.Hash) == 0 && len(data.HashFromEnv) > 0 { data.Hash = os.Getenv(data.HashFromEnv) @@ -129,6 +230,10 @@ func (p *password) UnmarshalJSON(b []byte) error { // OAuth2 describes enabled OAuth2 extensions. type OAuth2 struct { + // list of allowed grant types, + // defaults to all supported types + GrantTypes []string `json:"grantTypes"` + ResponseTypes []string `json:"responseTypes"` // If specified, do not prompt the user to approve client authorization. The // act of logging in implies authorization. @@ -137,15 +242,98 @@ type OAuth2 struct { AlwaysShowLoginScreen bool `json:"alwaysShowLoginScreen"` // This is the connector that can be used for password grant PasswordConnector string `json:"passwordConnector"` + // PKCE configuration + PKCE PKCE `json:"pkce"` +} + +// PKCE holds the PKCE (Proof Key for Code Exchange) configuration. +type PKCE struct { + // If true, PKCE is required for all authorization code flows. + Enforce bool `json:"enforce"` + // Supported code challenge methods. Defaults to ["S256", "plain"]. + CodeChallengeMethodsSupported []string `json:"codeChallengeMethodsSupported"` } // Web is the config format for the HTTP server. type Web struct { - HTTP string `json:"http"` - HTTPS string `json:"https"` - TLSCert string `json:"tlsCert"` - TLSKey string `json:"tlsKey"` - AllowedOrigins []string `json:"allowedOrigins"` + HTTP string `json:"http"` + HTTPS string `json:"https"` + Headers Headers `json:"headers"` + TLSCert string `json:"tlsCert"` + TLSKey string `json:"tlsKey"` + TLSMinVersion string `json:"tlsMinVersion"` + TLSMaxVersion string `json:"tlsMaxVersion"` + AllowedOrigins []string `json:"allowedOrigins"` + AllowedHeaders []string `json:"allowedHeaders"` + ClientRemoteIP ClientRemoteIP `json:"clientRemoteIP"` +} + +type ClientRemoteIP struct { + Header string `json:"header"` + TrustedProxies []string `json:"trustedProxies"` +} + +func (cr *ClientRemoteIP) ParseTrustedProxies() ([]netip.Prefix, error) { + if cr == nil { + return nil, nil + } + + trusted := make([]netip.Prefix, 0, len(cr.TrustedProxies)) + for _, cidr := range cr.TrustedProxies { + ipNet, err := netip.ParsePrefix(cidr) + if err != nil { + return nil, fmt.Errorf("failed to parse CIDR %q: %v", cidr, err) + } + trusted = append(trusted, ipNet) + } + + return trusted, nil +} + +type Headers struct { + // Set the Content-Security-Policy header to HTTP responses. + // Unset if blank. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + ContentSecurityPolicy string `json:"Content-Security-Policy"` + // Set the X-Frame-Options header to HTTP responses. + // Unset if blank. Accepted values are deny and sameorigin. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + XFrameOptions string `json:"X-Frame-Options"` + // Set the X-Content-Type-Options header to HTTP responses. + // Unset if blank. Accepted value is nosniff. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + XContentTypeOptions string `json:"X-Content-Type-Options"` + // Set the X-XSS-Protection header to all responses. + // Unset if blank. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + XXSSProtection string `json:"X-XSS-Protection"` + // Set the Strict-Transport-Security header to HTTP responses. + // Unset if blank. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + StrictTransportSecurity string `json:"Strict-Transport-Security"` +} + +func (h *Headers) ToHTTPHeader() http.Header { + if h == nil { + return make(map[string][]string) + } + header := make(map[string][]string) + if h.ContentSecurityPolicy != "" { + header["Content-Security-Policy"] = []string{h.ContentSecurityPolicy} + } + if h.XFrameOptions != "" { + header["X-Frame-Options"] = []string{h.XFrameOptions} + } + if h.XContentTypeOptions != "" { + header["X-Content-Type-Options"] = []string{h.XContentTypeOptions} + } + if h.XXSSProtection != "" { + header["X-XSS-Protection"] = []string{h.XXSSProtection} + } + if h.StrictTransportSecurity != "" { + header["Strict-Transport-Security"] = []string{h.StrictTransportSecurity} + } + return header } // Telemetry is the config format for telemetry including the HTTP server config. @@ -158,11 +346,13 @@ type Telemetry struct { // GRPC is the config for the gRPC API. type GRPC struct { // The port to listen on. - Addr string `json:"addr"` - TLSCert string `json:"tlsCert"` - TLSKey string `json:"tlsKey"` - TLSClientCA string `json:"tlsClientCA"` - Reflection bool `json:"reflection"` + Addr string `json:"addr"` + TLSCert string `json:"tlsCert"` + TLSKey string `json:"tlsKey"` + TLSClientCA string `json:"tlsClientCA"` + TLSMinVersion string `json:"tlsMinVersion"` + TLSMaxVersion string `json:"tlsMaxVersion"` + Reflection bool `json:"reflection"` } // Storage holds app's storage configuration. @@ -173,7 +363,7 @@ type Storage struct { // StorageConfig is a configuration that can create a storage. type StorageConfig interface { - Open(logger log.Logger) (storage.Storage, error) + Open(logger *slog.Logger) (storage.Storage, error) } var ( @@ -188,13 +378,32 @@ var ( _ StorageConfig = (*ent.MySQL)(nil) ) -func getORMBasedSQLStorage(normal, entBased StorageConfig) func() StorageConfig { +func getORMBasedSQLStorage(normal, entBased func() StorageConfig) func() StorageConfig { return func() StorageConfig { - switch os.Getenv("DEX_ENT_ENABLED") { - case "true", "yes": - return entBased - default: - return normal + if featureflags.EntEnabled.Enabled() { + return entBased() + } + return normal() + } +} + +// Recursively expand environment variables in the map to avoid +// issues with JSON special characters and escapes +func expandEnvInMap(m map[string]interface{}) { + for k, v := range m { + switch vt := v.(type) { + case string: + m[k] = os.ExpandEnv(vt) + case map[string]interface{}: + expandEnvInMap(vt) + case []interface{}: + for i, item := range vt { + if itemMap, ok := item.(map[string]interface{}); ok { + expandEnvInMap(itemMap) + } else if itemString, ok := item.(string); ok { + vt[i] = os.ExpandEnv(itemString) + } + } } } } @@ -203,22 +412,9 @@ var storages = map[string]func() StorageConfig{ "etcd": func() StorageConfig { return new(etcd.Etcd) }, "kubernetes": func() StorageConfig { return new(kubernetes.Config) }, "memory": func() StorageConfig { return new(memory.Config) }, - "sqlite3": getORMBasedSQLStorage(&sql.SQLite3{}, &ent.SQLite3{}), - "postgres": getORMBasedSQLStorage(&sql.Postgres{}, &ent.Postgres{}), - "mysql": getORMBasedSQLStorage(&sql.MySQL{}, &ent.MySQL{}), -} - -// isExpandEnvEnabled returns if os.ExpandEnv should be used for each storage and connector config. -// Disabling this feature avoids surprises e.g. if the LDAP bind password contains a dollar character. -// Returns false if the env variable "DEX_EXPAND_ENV" is a falsy string, e.g. "false". -// Returns true if the env variable is unset or a truthy string, e.g. "true", or can't be parsed as bool. -func isExpandEnvEnabled() bool { - enabled, err := strconv.ParseBool(os.Getenv("DEX_EXPAND_ENV")) - if err != nil { - // Unset, empty string or can't be parsed as bool: Default = true. - return true - } - return enabled + "sqlite3": getORMBasedSQLStorage(func() StorageConfig { return new(sql.SQLite3) }, func() StorageConfig { return new(ent.SQLite3) }), + "postgres": getORMBasedSQLStorage(func() StorageConfig { return new(sql.Postgres) }, func() StorageConfig { return new(ent.Postgres) }), + "mysql": getORMBasedSQLStorage(func() StorageConfig { return new(sql.MySQL) }, func() StorageConfig { return new(ent.MySQL) }), } // UnmarshalJSON allows Storage to implement the unmarshaler interface to @@ -228,7 +424,7 @@ func (s *Storage) UnmarshalJSON(b []byte) error { Type string `json:"type"` Config json.RawMessage `json:"config"` } - if err := json.Unmarshal(b, &store); err != nil { + if err := configUnmarshaller(b, &store); err != nil { return fmt.Errorf("parse storage: %v", err) } f, ok := storages[store.Type] @@ -239,11 +435,26 @@ func (s *Storage) UnmarshalJSON(b []byte) error { storageConfig := f() if len(store.Config) != 0 { data := []byte(store.Config) - if isExpandEnvEnabled() { - // Caution, we're expanding in the raw JSON/YAML source. This may not be what the admin expects. - data = []byte(os.ExpandEnv(string(store.Config))) + if featureflags.ExpandEnv.Enabled() { + var rawMap map[string]interface{} + if err := configUnmarshaller(store.Config, &rawMap); err != nil { + return fmt.Errorf("unmarshal config for env expansion: %v", err) + } + + // Recursively expand environment variables in the map to avoid + // issues with JSON special characters and escapes + expandEnvInMap(rawMap) + + // Marshal the expanded map back to JSON + expandedData, err := json.Marshal(rawMap) + if err != nil { + return fmt.Errorf("marshal expanded config: %v", err) + } + + data = expandedData } - if err := json.Unmarshal(data, storageConfig); err != nil { + + if err := configUnmarshaller(data, storageConfig); err != nil { return fmt.Errorf("parse storage config: %v", err) } } @@ -254,6 +465,74 @@ func (s *Storage) UnmarshalJSON(b []byte) error { return nil } +// Signer holds app's signer configuration. +type Signer struct { + Type string `json:"type"` + Config SignerConfig `json:"config"` +} + +// SignerConfig is a configuration that can create a signer. +type SignerConfig interface{} + +var ( + _ SignerConfig = (*signer.LocalConfig)(nil) + _ SignerConfig = (*signer.VaultConfig)(nil) +) + +var signerConfigs = map[string]func() SignerConfig{ + "local": func() SignerConfig { return new(signer.LocalConfig) }, + "vault": func() SignerConfig { return new(signer.VaultConfig) }, +} + +// UnmarshalJSON allows Signer to implement the unmarshaler interface to +// dynamically determine the type of the signer config. +func (s *Signer) UnmarshalJSON(b []byte) error { + var signerData struct { + Type string `json:"type"` + Config json.RawMessage `json:"config"` + } + if err := json.Unmarshal(b, &signerData); err != nil { + return fmt.Errorf("parse signer: %v", err) + } + + f, ok := signerConfigs[signerData.Type] + if !ok { + return fmt.Errorf("unknown signer type %q", signerData.Type) + } + + signerConfig := f() + if len(signerData.Config) != 0 { + data := []byte(signerData.Config) + if featureflags.ExpandEnv.Enabled() { + var rawMap map[string]interface{} + if err := json.Unmarshal(signerData.Config, &rawMap); err != nil { + return fmt.Errorf("unmarshal config for env expansion: %v", err) + } + + // Recursively expand environment variables in the map + expandEnvInMap(rawMap) + + // Marshal the expanded map back to JSON + expandedData, err := json.Marshal(rawMap) + if err != nil { + return fmt.Errorf("marshal expanded config: %v", err) + } + + data = expandedData + } + + if err := json.Unmarshal(data, signerConfig); err != nil { + return fmt.Errorf("parse signer config: %v", err) + } + } + + *s = Signer{ + Type: signerData.Type, + Config: signerConfig, + } + return nil +} + // Connector is a magical type that can unmarshal YAML dynamically. The // Type field determines the connector type, which is then customized for Config. type Connector struct { @@ -261,7 +540,8 @@ type Connector struct { Name string `json:"name"` ID string `json:"id"` - Config server.ConnectorConfig `json:"config"` + Config server.ConnectorConfig `json:"config"` + GrantTypes []string `json:"grantTypes"` } // UnmarshalJSON allows Connector to implement the unmarshaler interface to @@ -272,9 +552,10 @@ func (c *Connector) UnmarshalJSON(b []byte) error { Name string `json:"name"` ID string `json:"id"` - Config json.RawMessage `json:"config"` + Config json.RawMessage `json:"config"` + GrantTypes []string `json:"grantTypes"` } - if err := json.Unmarshal(b, &conn); err != nil { + if err := configUnmarshaller(b, &conn); err != nil { return fmt.Errorf("parse connector: %v", err) } f, ok := server.ConnectorsConfig[conn.Type] @@ -285,19 +566,36 @@ func (c *Connector) UnmarshalJSON(b []byte) error { connConfig := f() if len(conn.Config) != 0 { data := []byte(conn.Config) - if isExpandEnvEnabled() { - // Caution, we're expanding in the raw JSON/YAML source. This may not be what the admin expects. - data = []byte(os.ExpandEnv(string(conn.Config))) + if featureflags.ExpandEnv.Enabled() { + var rawMap map[string]interface{} + if err := configUnmarshaller(conn.Config, &rawMap); err != nil { + return fmt.Errorf("unmarshal config for env expansion: %v", err) + } + + // Recursively expand environment variables in the map to avoid + // issues with JSON special characters and escapes + expandEnvInMap(rawMap) + + // Marshal the expanded map back to JSON + expandedData, err := json.Marshal(rawMap) + if err != nil { + return fmt.Errorf("marshal expanded config: %v", err) + } + + data = expandedData } - if err := json.Unmarshal(data, connConfig); err != nil { + + if err := configUnmarshaller(data, connConfig); err != nil { return fmt.Errorf("parse connector config: %v", err) } } + *c = Connector{ - Type: conn.Type, - Name: conn.Name, - ID: conn.ID, - Config: connConfig, + Type: conn.Type, + Name: conn.Name, + ID: conn.ID, + Config: connConfig, + GrantTypes: conn.GrantTypes, } return nil } @@ -310,10 +608,11 @@ func ToStorageConnector(c Connector) (storage.Connector, error) { } return storage.Connector{ - ID: c.ID, - Type: c.Type, - Name: c.Name, - Config: data, + ID: c.ID, + Type: c.Type, + Name: c.Name, + Config: data, + GrantTypes: c.GrantTypes, }, nil } @@ -338,10 +637,16 @@ type Expiry struct { // Logger holds configuration required to customize logging for dex. type Logger struct { // Level sets logging level severity. - Level string `json:"level"` + Level slog.Level `json:"level"` // Format specifies the format to be used for logging. Format string `json:"format"` + + // ExcludeFields specifies log attribute keys that should be dropped from all + // log output. This is useful for suppressing PII fields like email, username, + // preferred_username, or groups in environments subject to GDPR or similar + // data-handling constraints. + ExcludeFields []string `json:"excludeFields"` } type RefreshToken struct { @@ -350,3 +655,32 @@ type RefreshToken struct { AbsoluteLifetime string `json:"absoluteLifetime"` ValidIfNotUsedFor string `json:"validIfNotUsedFor"` } + +// Sessions holds authentication session configuration. +type Sessions struct { + // CookieName is the name of the session cookie. Defaults to "dex_session". + CookieName string `json:"cookieName"` + // AbsoluteLifetime is the maximum session lifetime from creation. Defaults to "24h". + AbsoluteLifetime string `json:"absoluteLifetime"` + // ValidIfNotUsedFor is the idle timeout. Defaults to "1h". + ValidIfNotUsedFor string `json:"validIfNotUsedFor"` + // RememberMeCheckedByDefault controls the default state of the "remember me" checkbox. + RememberMeCheckedByDefault *bool `json:"rememberMeCheckedByDefault"` +} + +// MFAAuthenticator defines a multi-factor authentication provider. +type MFAAuthenticator struct { + ID string `json:"id"` + Type string `json:"type"` + Config json.RawMessage `json:"config"` + + // ConnectorTypes limits this authenticator to specific connector types (e.g., "ldap", "oidc", "saml"). + // If empty, the authenticator applies to all connector types. + ConnectorTypes []string `json:"connectorTypes"` +} + +// TOTPConfig holds configuration for a TOTP authenticator. +type TOTPConfig struct { + // Issuer is the name of the service shown in the authenticator app. + Issuer string `json:"issuer"` +} diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 8ee02d5aa2..26385f5649 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "log/slog" "os" "testing" @@ -10,12 +12,17 @@ import ( "github.com/dexidp/dex/connector/mock" "github.com/dexidp/dex/connector/oidc" "github.com/dexidp/dex/server" + "github.com/dexidp/dex/server/signer" "github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage/sql" ) var _ = yaml.YAMLToJSON +func boolPtr(v bool) *bool { + return &v +} + func TestValidConfiguration(t *testing.T) { configuration := Config{ Issuer: "http://127.0.0.1:5556/dex", @@ -37,6 +44,7 @@ func TestValidConfiguration(t *testing.T) { }, }, } + if err := configuration.Validate(); err != nil { t.Fatalf("this configuration should have been valid: %v", err) } @@ -71,7 +79,11 @@ storage: connMaxLifetime: 30 connectionTimeout: 3 web: - http: 127.0.0.1:5556 + https: 127.0.0.1:5556 + tlsMinVersion: 1.3 + tlsMaxVersion: 1.2 + headers: + Strict-Transport-Security: "max-age=31536000; includeSubDomains" frontend: dir: ./web @@ -87,11 +99,17 @@ staticClients: oauth2: alwaysShowLoginScreen: true + grantTypes: + - refresh_token + - "urn:ietf:params:oauth:grant-type:token-exchange" connectors: - type: mockCallback id: mock name: Example + grantTypes: + - authorization_code + - "urn:ietf:params:oauth:grant-type:token-exchange" - type: oidc id: google name: Google @@ -107,6 +125,12 @@ staticPasswords: # bcrypt hash of the string "password" hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy" username: "admin" + name: "Admin User" + emailVerified: false + preferredUsername: "admin-public" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" - email: "foo@example.com" # base64'd value of the same bcrypt hash above. We want to be able to parse both of these @@ -123,6 +147,10 @@ expiry: logger: level: "debug" format: "json" + +additionalFeatures: [ + "ConnectorsCRUD" +] `) want := Config{ @@ -141,7 +169,12 @@ logger: }, }, Web: Web{ - HTTP: "127.0.0.1:5556", + HTTPS: "127.0.0.1:5556", + TLSMinVersion: "1.3", + TLSMaxVersion: "1.2", + Headers: Headers{ + StrictTransportSecurity: "max-age=31536000; includeSubDomains", + }, }, Frontend: server.WebConfig{ Dir: "./web", @@ -161,6 +194,10 @@ logger: }, OAuth2: OAuth2{ AlwaysShowLoginScreen: true, + GrantTypes: []string{ + "refresh_token", + "urn:ietf:params:oauth:grant-type:token-exchange", + }, }, StaticConnectors: []Connector{ { @@ -168,6 +205,10 @@ logger: ID: "mock", Name: "Example", Config: &mock.CallbackConfig{}, + GrantTypes: []string{ + "authorization_code", + "urn:ietf:params:oauth:grant-type:token-exchange", + }, }, { Type: "oidc", @@ -184,10 +225,17 @@ logger: EnablePasswordDB: true, StaticPasswords: []password{ { - Email: "admin@example.com", - Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"), - Username: "admin", - UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466", + Email: "admin@example.com", + Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"), + Username: "admin", + Name: "Admin User", + EmailVerified: boolPtr(false), + PreferredUsername: "admin-public", + UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466", + Groups: []string{ + "team-a", + "team-a/admins", + }, }, { Email: "foo@example.com", @@ -203,7 +251,7 @@ logger: DeviceRequests: "10m", }, Logger: Logger{ - Level: "debug", + Level: slog.LevelDebug, Format: "json", }, } @@ -212,6 +260,7 @@ logger: if err := yaml.Unmarshal(rawConfig, &c); err != nil { t.Fatalf("failed to decode config: %v", err) } + if diff := pretty.Compare(c, want); diff != "" { t.Errorf("got!=want: %s", diff) } @@ -250,7 +299,8 @@ func checkUnmarshalConfigWithEnv(t *testing.T, dexExpandEnv string, wantExpandEn os.Setenv("DEX_FOO_USER_PASSWORD", "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy") // For os.ExpandEnv ($VAR -> value_of_VAR): os.Setenv("DEX_FOO_POSTGRES_HOST", "10.0.0.1") - os.Setenv("DEX_FOO_OIDC_CLIENT_SECRET", "bar") + os.Setenv("DEX_FOO_POSTGRES_PASSWORD", `psql"test\pass`) + os.Setenv("DEX_FOO_OIDC_CLIENT_SECRET", `abc"def\ghi`) if dexExpandEnv != "UNSET" { os.Setenv("DEX_EXPAND_ENV", dexExpandEnv) } else { @@ -265,6 +315,7 @@ storage: # Env variables are expanded in raw YAML source. # Single quotes work fine, as long as the env variable doesn't contain any. host: '$DEX_FOO_POSTGRES_HOST' + password: '$DEX_FOO_POSTGRES_PASSWORD' port: 65432 maxOpenConns: 5 maxIdleConns: 3 @@ -327,10 +378,12 @@ logger: // This is not a valid hostname. It's only used to check whether os.ExpandEnv was applied or not. wantPostgresHost := "$DEX_FOO_POSTGRES_HOST" + wantPostgresPassword := "$DEX_FOO_POSTGRES_PASSWORD" wantOidcClientSecret := "$DEX_FOO_OIDC_CLIENT_SECRET" if wantExpandEnv { wantPostgresHost = "10.0.0.1" - wantOidcClientSecret = "bar" + wantPostgresPassword = `psql"test\pass` + wantOidcClientSecret = `abc"def\ghi` } want := Config{ @@ -340,6 +393,7 @@ logger: Config: &sql.Postgres{ NetworkDB: sql.NetworkDB{ Host: wantPostgresHost, + Password: wantPostgresPassword, Port: 65432, MaxOpenConns: 5, MaxIdleConns: 3, @@ -410,7 +464,7 @@ logger: AuthRequests: "25h", }, Logger: Logger{ - Level: "debug", + Level: slog.LevelDebug, Format: "json", }, } @@ -419,7 +473,122 @@ logger: if err := yaml.Unmarshal(rawConfig, &c); err != nil { t.Fatalf("failed to decode config: %v", err) } + if diff := pretty.Compare(c, want); diff != "" { t.Errorf("got!=want: %s", diff) } } + +func TestSignerConfigUnmarshal(t *testing.T) { + tests := []struct { + name string + config string + wantErr bool + check func(*Config) error + }{ + { + name: "local signer with rotation period", + config: ` +issuer: http://127.0.0.1:5556/dex +storage: + type: memory +web: + http: 0.0.0.0:5556 +signer: + type: local + config: + keysRotationPeriod: 6h +enablePasswordDB: true +`, + wantErr: false, + check: func(c *Config) error { + if c.Signer.Type != "local" { + t.Errorf("expected signer type 'local', got %q", c.Signer.Type) + } + if localConfig, ok := c.Signer.Config.(*signer.LocalConfig); !ok { + t.Error("expected LocalConfig") + } else if localConfig.KeysRotationPeriod != "6h" { + t.Errorf("expected keys rotation period '6h', got %q", localConfig.KeysRotationPeriod) + } + return nil + }, + }, + { + name: "vault signer", + config: ` +issuer: http://127.0.0.1:5556/dex +storage: + type: memory +web: + http: 0.0.0.0:5556 +signer: + type: vault + config: + addr: http://localhost:8200 + token: test-token + keyName: test-key +enablePasswordDB: true +`, + wantErr: false, + check: func(c *Config) error { + if c.Signer.Type != "vault" { + t.Errorf("expected signer type 'vault', got %q", c.Signer.Type) + } + if vaultConfig, ok := c.Signer.Config.(*signer.VaultConfig); !ok { + t.Error("expected VaultConfig") + } else { + if vaultConfig.Addr != "http://localhost:8200" { + t.Errorf("expected addr 'http://localhost:8200', got %q", vaultConfig.Addr) + } + if vaultConfig.Token != "test-token" { + t.Errorf("expected token 'test-token', got %q", vaultConfig.Token) + } + if vaultConfig.KeyName != "test-key" { + t.Errorf("expected keyName 'test-key', got %q", vaultConfig.KeyName) + } + } + return nil + }, + }, + { + name: "default to local when no signer specified", + config: ` +issuer: http://127.0.0.1:5556/dex +storage: + type: memory +web: + http: 0.0.0.0:5556 +enablePasswordDB: true +`, + wantErr: false, + check: func(c *Config) error { + if c.Signer.Type != "" { + t.Errorf("expected signer type '', got %q", c.Signer.Type) + } + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var c Config + data, err := yaml.YAMLToJSON([]byte(tt.config)) + if err != nil { + t.Fatalf("failed to convert yaml to json: %v", err) + } + + err = json.Unmarshal(data, &c) + if (err != nil) != tt.wantErr { + t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err == nil && tt.check != nil { + if err := tt.check(&c); err != nil { + t.Errorf("check failed: %v", err) + } + } + }) + } +} diff --git a/cmd/dex/excluding_handler.go b/cmd/dex/excluding_handler.go new file mode 100644 index 0000000000..c5d03e44e5 --- /dev/null +++ b/cmd/dex/excluding_handler.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "log/slog" +) + +// excludingHandler is an slog.Handler wrapper that drops log attributes +// whose keys match a configured set. This allows PII fields like email, +// username, or groups to be redacted at the logger level rather than +// requiring per-callsite suppression logic. +type excludingHandler struct { + inner slog.Handler + exclude map[string]bool +} + +func newExcludingHandler(inner slog.Handler, fields []string) slog.Handler { + if len(fields) == 0 { + return inner + } + m := make(map[string]bool, len(fields)) + for _, f := range fields { + m[f] = true + } + return &excludingHandler{inner: inner, exclude: m} +} + +func (h *excludingHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.inner.Enabled(ctx, level) +} + +func (h *excludingHandler) Handle(ctx context.Context, record slog.Record) error { + // Rebuild the record without excluded attributes. + filtered := slog.NewRecord(record.Time, record.Level, record.Message, record.PC) + record.Attrs(func(a slog.Attr) bool { + if !h.exclude[a.Key] { + filtered.AddAttrs(a) + } + return true + }) + return h.inner.Handle(ctx, filtered) +} + +func (h *excludingHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + var kept []slog.Attr + for _, a := range attrs { + if !h.exclude[a.Key] { + kept = append(kept, a) + } + } + return &excludingHandler{inner: h.inner.WithAttrs(kept), exclude: h.exclude} +} + +func (h *excludingHandler) WithGroup(name string) slog.Handler { + return &excludingHandler{inner: h.inner.WithGroup(name), exclude: h.exclude} +} diff --git a/cmd/dex/excluding_handler_test.go b/cmd/dex/excluding_handler_test.go new file mode 100644 index 0000000000..e0306d6034 --- /dev/null +++ b/cmd/dex/excluding_handler_test.go @@ -0,0 +1,141 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "testing" +) + +func TestExcludingHandler(t *testing.T) { + tests := []struct { + name string + exclude []string + logAttrs []slog.Attr + wantKeys []string + absentKeys []string + }{ + { + name: "no exclusions", + exclude: nil, + logAttrs: []slog.Attr{ + slog.String("email", "user@example.com"), + slog.String("connector_id", "github"), + }, + wantKeys: []string{"email", "connector_id"}, + }, + { + name: "exclude email", + exclude: []string{"email"}, + logAttrs: []slog.Attr{ + slog.String("email", "user@example.com"), + slog.String("connector_id", "github"), + }, + wantKeys: []string{"connector_id"}, + absentKeys: []string{"email"}, + }, + { + name: "exclude multiple fields", + exclude: []string{"email", "username", "groups"}, + logAttrs: []slog.Attr{ + slog.String("email", "user@example.com"), + slog.String("username", "johndoe"), + slog.String("connector_id", "github"), + slog.Any("groups", []string{"admin"}), + }, + wantKeys: []string{"connector_id"}, + absentKeys: []string{"email", "username", "groups"}, + }, + { + name: "exclude non-existent field is harmless", + exclude: []string{"nonexistent"}, + logAttrs: []slog.Attr{ + slog.String("email", "user@example.com"), + }, + wantKeys: []string{"email"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + inner := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) + handler := newExcludingHandler(inner, tt.exclude) + logger := slog.New(handler) + + attrs := make([]any, 0, len(tt.logAttrs)*2) + for _, a := range tt.logAttrs { + attrs = append(attrs, a) + } + logger.Info("test message", attrs...) + + var result map[string]any + if err := json.Unmarshal(buf.Bytes(), &result); err != nil { + t.Fatalf("failed to parse log output: %v", err) + } + + for _, key := range tt.wantKeys { + if _, ok := result[key]; !ok { + t.Errorf("expected key %q in log output", key) + } + } + for _, key := range tt.absentKeys { + if _, ok := result[key]; ok { + t.Errorf("expected key %q to be absent from log output", key) + } + } + }) + } +} + +func TestExcludingHandlerWithAttrs(t *testing.T) { + var buf bytes.Buffer + inner := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) + handler := newExcludingHandler(inner, []string{"email"}) + logger := slog.New(handler) + + // Pre-bind an excluded attr via With + child := logger.With("email", "user@example.com", "connector_id", "github") + child.Info("login successful") + + var result map[string]any + if err := json.Unmarshal(buf.Bytes(), &result); err != nil { + t.Fatalf("failed to parse log output: %v", err) + } + + if _, ok := result["email"]; ok { + t.Error("expected email to be excluded from WithAttrs output") + } + if _, ok := result["connector_id"]; !ok { + t.Error("expected connector_id to be present") + } +} + +func TestExcludingHandlerEnabled(t *testing.T) { + inner := slog.NewJSONHandler(&bytes.Buffer{}, &slog.HandlerOptions{Level: slog.LevelWarn}) + handler := newExcludingHandler(inner, []string{"email"}) + + if handler.Enabled(context.Background(), slog.LevelInfo) { + t.Error("expected Info to be disabled when handler level is Warn") + } + if !handler.Enabled(context.Background(), slog.LevelWarn) { + t.Error("expected Warn to be enabled") + } +} + +func TestExcludingHandlerNilFields(t *testing.T) { + var buf bytes.Buffer + inner := slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}) + + // With nil/empty fields, should return the inner handler directly + handler := newExcludingHandler(inner, nil) + if _, ok := handler.(*excludingHandler); ok { + t.Error("expected nil fields to return inner handler directly, not wrap it") + } + + handler = newExcludingHandler(inner, []string{}) + if _, ok := handler.(*excludingHandler); ok { + t.Error("expected empty fields to return inner handler directly, not wrap it") + } +} diff --git a/cmd/dex/logger.go b/cmd/dex/logger.go new file mode 100644 index 0000000000..fd4bd29426 --- /dev/null +++ b/cmd/dex/logger.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "strings" + + "github.com/dexidp/dex/server" +) + +var logFormats = []string{"json", "text"} + +func newLogger(level slog.Level, format string, excludeFields []string) (*slog.Logger, error) { + var handler slog.Handler + switch strings.ToLower(format) { + case "", "text": + handler = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: level, + }) + case "json": + handler = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: level, + }) + default: + return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format) + } + + handler = newExcludingHandler(handler, excludeFields) + + return slog.New(newRequestContextHandler(handler)), nil +} + +var _ slog.Handler = requestContextHandler{} + +type requestContextHandler struct { + handler slog.Handler +} + +func newRequestContextHandler(handler slog.Handler) slog.Handler { + return requestContextHandler{ + handler: handler, + } +} + +func (h requestContextHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.handler.Enabled(ctx, level) +} + +func (h requestContextHandler) Handle(ctx context.Context, record slog.Record) error { + if v, ok := ctx.Value(server.RequestKeyRemoteIP).(string); ok { + record.AddAttrs(slog.String(string(server.RequestKeyRemoteIP), v)) + } + + if v, ok := ctx.Value(server.RequestKeyRequestID).(string); ok { + record.AddAttrs(slog.String(string(server.RequestKeyRequestID), v)) + } + + return h.handler.Handle(ctx, record) +} + +func (h requestContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return requestContextHandler{h.handler.WithAttrs(attrs)} +} + +func (h requestContextHandler) WithGroup(name string) slog.Handler { + return requestContextHandler{h.handler.WithGroup(name)} +} diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index c8fb95eb16..995ae0fc75 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -4,35 +4,41 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "errors" "fmt" + "log/slog" "net" "net/http" "net/http/pprof" "os" + "os/signal" + "path/filepath" "runtime" "strings" + "sync/atomic" "syscall" "time" gosundheit "github.com/AppsFlyer/go-sundheit" "github.com/AppsFlyer/go-sundheit/checks" gosundheithttp "github.com/AppsFlyer/go-sundheit/http" + "github.com/fsnotify/fsnotify" "github.com/ghodss/yaml" grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "github.com/oklog/run" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/reflection" "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/featureflags" "github.com/dexidp/dex/server" + "github.com/dexidp/dex/server/signer" "github.com/dexidp/dex/storage" ) @@ -47,6 +53,15 @@ type serveOptions struct { grpcAddr string } +var buildInfo = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "build_info", + Namespace: "dex", + Help: "A metric with a constant '1' value labeled by version from which Dex was built.", + }, + []string{"version", "go_version", "platform"}, +) + func commandServe() *cobra.Command { options := serveOptions{} @@ -83,35 +98,47 @@ func runServe(options serveOptions) error { } var c Config - if err := yaml.Unmarshal(configData, &c); err != nil { + + jsonConfigData, err := yaml.YAMLToJSON(configData) + if err != nil { return fmt.Errorf("error parse config file %s: %v", configFile, err) } + if err := configUnmarshaller(jsonConfigData, &c); err != nil { + return fmt.Errorf("error unmarshalling config file %s: %v", configFile, err) + } + applyConfigOverrides(options, &c) - logger, err := newLogger(c.Logger.Level, c.Logger.Format) + logger, err := newLogger(c.Logger.Level, c.Logger.Format, c.Logger.ExcludeFields) if err != nil { return fmt.Errorf("invalid config: %v", err) } - logger.Infof( - "Dex Version: %s, Go Version: %s, Go OS/ARCH: %s %s", - version, - runtime.Version(), - runtime.GOOS, - runtime.GOARCH, + logger.Info( + "Version info", + "dex_version", version, + slog.Group("go", + "version", runtime.Version(), + "os", runtime.GOOS, + "arch", runtime.GOARCH, + ), ) - if c.Logger.Level != "" { - logger.Infof("config using log level: %s", c.Logger.Level) + if c.Logger.Level != slog.LevelInfo { + logger.Info("config using log level", "level", c.Logger.Level) } if err := c.Validate(); err != nil { return err } - logger.Infof("config issuer: %s", c.Issuer) + logger.Info("config issuer", "issuer", c.Issuer) prometheusRegistry := prometheus.NewRegistry() + + prometheusRegistry.MustRegister(buildInfo) + recordBuildInfo() + err = prometheusRegistry.Register(collectors.NewGoCollector()) if err != nil { return fmt.Errorf("failed to register Go runtime metrics: %v", err) @@ -141,34 +168,33 @@ func runServe(options serveOptions) error { tls.TLS_RSA_WITH_AES_256_GCM_SHA384, } + allowedTLSVersions := map[string]int{ + "1.2": tls.VersionTLS12, + "1.3": tls.VersionTLS13, + } + if c.GRPC.TLSCert != "" { - // Parse certificates from certificate file and key file for server. - cert, err := tls.LoadX509KeyPair(c.GRPC.TLSCert, c.GRPC.TLSKey) - if err != nil { - return fmt.Errorf("invalid config: error parsing gRPC certificate file: %v", err) + tlsMinVersion := tls.VersionTLS12 + if c.GRPC.TLSMinVersion != "" { + tlsMinVersion = allowedTLSVersions[c.GRPC.TLSMinVersion] } - - tlsConfig := tls.Config{ - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS12, + tlsMaxVersion := 0 // default for max is whatever Go defaults to + if c.GRPC.TLSMaxVersion != "" { + tlsMaxVersion = allowedTLSVersions[c.GRPC.TLSMaxVersion] + } + baseTLSConfig := &tls.Config{ + MinVersion: uint16(tlsMinVersion), + MaxVersion: uint16(tlsMaxVersion), CipherSuites: allowedTLSCiphers, PreferServerCipherSuites: true, } - if c.GRPC.TLSClientCA != "" { - // Parse certificates from client CA file to a new CertPool. - cPool := x509.NewCertPool() - clientCert, err := os.ReadFile(c.GRPC.TLSClientCA) - if err != nil { - return fmt.Errorf("invalid config: reading from client CA file: %v", err) - } - if !cPool.AppendCertsFromPEM(clientCert) { - return errors.New("invalid config: failed to parse client CA") - } - - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - tlsConfig.ClientCAs = cPool + tlsConfig, err := newTLSReloader(logger, c.GRPC.TLSCert, c.GRPC.TLSKey, c.GRPC.TLSClientCA, baseTLSConfig) + if err != nil { + return fmt.Errorf("invalid config: get gRPC TLS: %v", err) + } + if c.GRPC.TLSClientCA != "" { // Only add metrics if client auth is enabled grpcOptions = append(grpcOptions, grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()), @@ -176,7 +202,7 @@ func runServe(options serveOptions) error { ) } - grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(&tlsConfig))) + grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(tlsConfig))) } s, err := c.Storage.Config.Open(logger) @@ -185,7 +211,7 @@ func runServe(options serveOptions) error { } defer s.Close() - logger.Infof("config storage: %s", c.Storage.Type) + logger.Info("config storage", "storage_type", c.Storage.Type) if len(c.StaticClients) > 0 { for i, client := range c.StaticClients { @@ -210,7 +236,7 @@ func runServe(options serveOptions) error { } c.StaticClients[i].Secret = os.Getenv(client.SecretEnv) } - logger.Infof("config static client: %s", client.Name) + logger.Info("config static client", "client_name", client.Name) } s = storage.WithStaticClients(s, c.StaticClients) } @@ -230,7 +256,12 @@ func runServe(options serveOptions) error { if c.Config == nil { return fmt.Errorf("invalid config: no config field for connector %q", c.ID) } - logger.Infof("config connector: %s", c.ID) + for _, gt := range c.GrantTypes { + if !server.ConnectorGrantTypes[gt] { + return fmt.Errorf("invalid config: unknown grant type %q for connector %q", gt, c.ID) + } + } + logger.Info("config connector", "connector_id", c.ID) // convert to a storage connector object conn, err := ToStorageConnector(c) @@ -246,22 +277,25 @@ func runServe(options serveOptions) error { Name: "Email", Type: server.LocalConnector, }) - logger.Infof("config connector: local passwords enabled") + logger.Info("config connector: local passwords enabled") } s = storage.WithStaticConnectors(s, storageConnectors) if len(c.OAuth2.ResponseTypes) > 0 { - logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes) + logger.Info("config response types accepted", "response_types", c.OAuth2.ResponseTypes) } if c.OAuth2.SkipApprovalScreen { - logger.Infof("config skipping approval screen") + logger.Info("config skipping approval screen") } if c.OAuth2.PasswordConnector != "" { - logger.Infof("config using password grant connector: %s", c.OAuth2.PasswordConnector) + logger.Info("config using password grant connector", "password_connector", c.OAuth2.PasswordConnector) } if len(c.Web.AllowedOrigins) > 0 { - logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins) + logger.Info("config allowed origins", "origins", c.Web.AllowedOrigins) + } + if featureflags.ContinueOnConnectorFailure.Enabled() { + logger.Info("continue on connector failure feature flag enabled") } // explicitly convert to UTC. @@ -269,50 +303,106 @@ func runServe(options serveOptions) error { healthChecker := gosundheit.New() - serverConfig := server.Config{ - SupportedResponseTypes: c.OAuth2.ResponseTypes, - SkipApprovalScreen: c.OAuth2.SkipApprovalScreen, - AlwaysShowLoginScreen: c.OAuth2.AlwaysShowLoginScreen, - PasswordConnector: c.OAuth2.PasswordConnector, - AllowedOrigins: c.Web.AllowedOrigins, - Issuer: c.Issuer, - Storage: s, - Web: c.Frontend, - Logger: logger, - Now: now, - PrometheusRegistry: prometheusRegistry, - HealthChecker: healthChecker, - } - if c.Expiry.SigningKeys != "" { - signingKeys, err := time.ParseDuration(c.Expiry.SigningKeys) + // Parse expiry durations + idTokensValidFor := 24 * time.Hour // default + if c.Expiry.IDTokens != "" { + var err error + idTokensValidFor, err = time.ParseDuration(c.Expiry.IDTokens) if err != nil { - return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err) + return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err) } - logger.Infof("config signing keys expire after: %v", signingKeys) - serverConfig.RotateKeysAfter = signingKeys + logger.Info("config id tokens", "valid_for", idTokensValidFor) } - if c.Expiry.IDTokens != "" { - idTokens, err := time.ParseDuration(c.Expiry.IDTokens) + + // Create signer + var signerInstance signer.Signer + switch c.Signer.Type { + case "vault": + vaultConfig, ok := c.Signer.Config.(*signer.VaultConfig) + if !ok { + return fmt.Errorf("invalid vault signer config") + } + signerInstance, err = vaultConfig.Open(context.Background()) if err != nil { - return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err) + return fmt.Errorf("failed to open vault signer: %v", err) } - logger.Infof("config id tokens valid for: %v", idTokens) - serverConfig.IDTokensValidFor = idTokens + logger.Info("signer configured", "type", "vault") + case "local": + localConfig, ok := c.Signer.Config.(*signer.LocalConfig) + if !ok { + return fmt.Errorf("invalid local signer config") + } + if localConfig.KeysRotationPeriod == "" { + return fmt.Errorf("failed to open local signer: signer.config.keysRotationPeriod must be specified") + } + if c.Expiry.SigningKeys != "" { + logger.Warn("both expiry.signingKeys and signer.config.keysRotationPeriod specified, using signer.config.keysRotationPeriod") + } + signerInstance, err = localConfig.Open(context.Background(), s, idTokensValidFor, now, logger) + if err != nil { + return fmt.Errorf("failed to open local signer: %v", err) + } + logger.Info("signer configured", "type", "local", "keys_rotation_period", localConfig.KeysRotationPeriod) + case "": // Default to local signer + // Handle deprecated expiry.signingKeys configuration + if c.Expiry.SigningKeys != "" { + logger.Warn("config expiry.signingKeys will be removed in a future release", + "use_instead", "signer.config.keysRotationPeriod", + "current_value", c.Expiry.SigningKeys, "deprecated", true) + } else { + c.Expiry.SigningKeys = "6h" + } + localConfig := signer.LocalConfig{KeysRotationPeriod: c.Expiry.SigningKeys} + signerInstance, err = localConfig.Open(context.Background(), s, idTokensValidFor, now, logger) + if err != nil { + return fmt.Errorf("failed to open local signer: %v", err) + } + logger.Info("signer configured", "type", "local", "keys_rotation_period", localConfig.KeysRotationPeriod) + default: + return fmt.Errorf("unknown signer type %q", c.Signer.Type) } + + serverConfig := server.Config{ + AllowedGrantTypes: c.OAuth2.GrantTypes, + SupportedResponseTypes: c.OAuth2.ResponseTypes, + SkipApprovalScreen: c.OAuth2.SkipApprovalScreen, + AlwaysShowLoginScreen: c.OAuth2.AlwaysShowLoginScreen, + PasswordConnector: c.OAuth2.PasswordConnector, + PKCE: server.PKCEConfig{ + Enforce: c.OAuth2.PKCE.Enforce, + CodeChallengeMethodsSupported: c.OAuth2.PKCE.CodeChallengeMethodsSupported, + }, + Headers: c.Web.Headers.ToHTTPHeader(), + AllowedOrigins: c.Web.AllowedOrigins, + AllowedHeaders: c.Web.AllowedHeaders, + Issuer: c.Issuer, + Storage: s, + Web: c.Frontend, + Logger: logger, + Now: now, + PrometheusRegistry: prometheusRegistry, + HealthChecker: healthChecker, + ContinueOnConnectorFailure: featureflags.ContinueOnConnectorFailure.Enabled(), + Signer: signerInstance, + IDTokensValidFor: idTokensValidFor, + MFAProviders: buildMFAProviders(c.MFA.Authenticators, logger), + DefaultMFAChain: c.MFA.DefaultMFAChain, + } + if c.Expiry.AuthRequests != "" { authRequests, err := time.ParseDuration(c.Expiry.AuthRequests) if err != nil { return fmt.Errorf("invalid config value %q for auth request expiry: %v", c.Expiry.AuthRequests, err) } - logger.Infof("config auth requests valid for: %v", authRequests) + logger.Info("config auth requests", "valid_for", authRequests) serverConfig.AuthRequestsValidFor = authRequests } if c.Expiry.DeviceRequests != "" { deviceRequests, err := time.ParseDuration(c.Expiry.DeviceRequests) if err != nil { - return fmt.Errorf("invalid config value %q for device request expiry: %v", c.Expiry.AuthRequests, err) + return fmt.Errorf("invalid config value %q for device request expiry: %v", c.Expiry.DeviceRequests, err) } - logger.Infof("config device requests valid for: %v", deviceRequests) + logger.Info("config device requests", "valid_for", deviceRequests) serverConfig.DeviceRequestsValidFor = deviceRequests } refreshTokenPolicy, err := server.NewRefreshTokenPolicy( @@ -327,6 +417,26 @@ func runServe(options serveOptions) error { } serverConfig.RefreshTokenPolicy = refreshTokenPolicy + + if featureflags.SessionsEnabled.Enabled() { + sessionConfig, err := parseSessionConfig(c.Sessions) + if err != nil { + return fmt.Errorf("invalid session config: %v", err) + } + serverConfig.SessionConfig = sessionConfig + logger.Info("config sessions", + "cookie_name", sessionConfig.CookieName, + "absolute_lifetime", sessionConfig.AbsoluteLifetime, + "valid_if_not_used_for", sessionConfig.ValidIfNotUsedFor, + ) + } + + serverConfig.RealIPHeader = c.Web.ClientRemoteIP.Header + serverConfig.TrustedRealIPCIDRs, err = c.Web.ClientRemoteIP.ParseTrustedProxies() + if err != nil { + return fmt.Errorf("failed to parse client remote IP settings: %v", err) + } + serv, err := server.NewServer(context.Background(), serverConfig) if err != nil { return fmt.Errorf("failed to initialize server: %v", err) @@ -362,7 +472,7 @@ func runServe(options serveOptions) error { if c.Telemetry.HTTP != "" { const name = "telemetry" - logger.Infof("listening (%s) on %s", name, c.Telemetry.HTTP) + logger.Info("listening on", "server", name, "address", c.Telemetry.HTTP) l, err := net.Listen("tcp", c.Telemetry.HTTP) if err != nil { @@ -384,9 +494,9 @@ func runServe(options serveOptions) error { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - logger.Debugf("starting graceful shutdown (%s)", name) + logger.Debug("starting graceful shutdown", "server", name) if err := server.Shutdown(ctx); err != nil { - logger.Errorf("graceful shutdown (%s): %v", name, err) + logger.Error("graceful shutdown", "server", name, "err", err) } }) } @@ -395,7 +505,7 @@ func runServe(options serveOptions) error { if c.Web.HTTP != "" { const name = "http" - logger.Infof("listening (%s) on %s", name, c.Web.HTTP) + logger.Info("listening on", "server", name, "address", c.Web.HTTP) l, err := net.Listen("tcp", c.Web.HTTP) if err != nil { @@ -413,9 +523,9 @@ func runServe(options serveOptions) error { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - logger.Debugf("starting graceful shutdown (%s)", name) + logger.Debug("starting graceful shutdown", "server", name) if err := server.Shutdown(ctx); err != nil { - logger.Errorf("graceful shutdown (%s): %v", name, err) + logger.Error("graceful shutdown", "server", name, "err", err) } }) } @@ -424,47 +534,64 @@ func runServe(options serveOptions) error { if c.Web.HTTPS != "" { const name = "https" - logger.Infof("listening (%s) on %s", name, c.Web.HTTPS) + logger.Info("listening on", "server", name, "address", c.Web.HTTPS) l, err := net.Listen("tcp", c.Web.HTTPS) if err != nil { return fmt.Errorf("listening (%s) on %s: %v", name, c.Web.HTTPS, err) } + tlsMinVersion := tls.VersionTLS12 + if c.Web.TLSMinVersion != "" { + tlsMinVersion = allowedTLSVersions[c.Web.TLSMinVersion] + } + tlsMaxVersion := 0 // default for max is whatever Go defaults to + if c.Web.TLSMaxVersion != "" { + tlsMaxVersion = allowedTLSVersions[c.Web.TLSMaxVersion] + } + + baseTLSConfig := &tls.Config{ + MinVersion: uint16(tlsMinVersion), + MaxVersion: uint16(tlsMaxVersion), + CipherSuites: allowedTLSCiphers, + PreferServerCipherSuites: true, + } + + tlsConfig, err := newTLSReloader(logger, c.Web.TLSCert, c.Web.TLSKey, "", baseTLSConfig) + if err != nil { + return fmt.Errorf("invalid config: get HTTP TLS: %v", err) + } + server := &http.Server{ - Handler: serv, - TLSConfig: &tls.Config{ - CipherSuites: allowedTLSCiphers, - PreferServerCipherSuites: true, - MinVersion: tls.VersionTLS12, - }, + Handler: serv, + TLSConfig: tlsConfig, } defer server.Close() group.Add(func() error { - return server.ServeTLS(l, c.Web.TLSCert, c.Web.TLSKey) + return server.ServeTLS(l, "", "") }, func(err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - logger.Debugf("starting graceful shutdown (%s)", name) + logger.Debug("starting graceful shutdown", "server", name) if err := server.Shutdown(ctx); err != nil { - logger.Errorf("graceful shutdown (%s): %v", name, err) + logger.Error("graceful shutdown", "server", name, "err", err) } }) } // Set up grpc server if c.GRPC.Addr != "" { - logger.Infof("listening (grpc) on %s", c.GRPC.Addr) + logger.Info("listening on", "server", "grpc", "address", c.GRPC.Addr) grpcListener, err := net.Listen("tcp", c.GRPC.Addr) if err != nil { - return fmt.Errorf("listening (grcp) on %s: %w", c.GRPC.Addr, err) + return fmt.Errorf("listening (grpc) on %s: %w", c.GRPC.Addr, err) } grpcSrv := grpc.NewServer(grpcOptions...) - api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version)) + api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version, serv)) grpcMetrics.InitializeMetrics(grpcSrv) if c.GRPC.Reflection { @@ -475,7 +602,7 @@ func runServe(options serveOptions) error { group.Add(func() error { return grpcSrv.Serve(grpcListener) }, func(err error) { - logger.Debugf("starting graceful shutdown (grpc)") + logger.Debug("starting graceful shutdown", "server", "grpc") grpcSrv.GracefulStop() }) } @@ -485,55 +612,11 @@ func runServe(options serveOptions) error { if _, ok := err.(run.SignalError); !ok { return fmt.Errorf("run groups: %w", err) } - logger.Infof("%v, shutdown now", err) + logger.Info("shutdown now", "err", err) } return nil } -var ( - logLevels = []string{"debug", "info", "error"} - logFormats = []string{"json", "text"} -) - -type utcFormatter struct { - f logrus.Formatter -} - -func (f *utcFormatter) Format(e *logrus.Entry) ([]byte, error) { - e.Time = e.Time.UTC() - return f.f.Format(e) -} - -func newLogger(level string, format string) (log.Logger, error) { - var logLevel logrus.Level - switch strings.ToLower(level) { - case "debug": - logLevel = logrus.DebugLevel - case "", "info": - logLevel = logrus.InfoLevel - case "error": - logLevel = logrus.ErrorLevel - default: - return nil, fmt.Errorf("log level is not one of the supported values (%s): %s", strings.Join(logLevels, ", "), level) - } - - var formatter utcFormatter - switch strings.ToLower(format) { - case "", "text": - formatter.f = &logrus.TextFormatter{DisableColors: true} - case "json": - formatter.f = &logrus.JSONFormatter{} - default: - return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format) - } - - return &logrus.Logger{ - Out: os.Stderr, - Formatter: &formatter, - Level: logLevel, - }, nil -} - func applyConfigOverrides(options serveOptions, config *Config) { if options.webHTTPAddr != "" { config.Web.HTTP = options.webHTTPAddr @@ -554,6 +637,20 @@ func applyConfigOverrides(options serveOptions, config *Config) { if config.Frontend.Dir == "" { config.Frontend.Dir = os.Getenv("DEX_FRONTEND_DIR") } + + if len(config.OAuth2.GrantTypes) == 0 { + config.OAuth2.GrantTypes = []string{ + "authorization_code", + "implicit", + "password", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:ietf:params:oauth:grant-type:token-exchange", + } + if featureflags.ClientCredentialGrantEnabledByDefault.Enabled() { + config.OAuth2.GrantTypes = append(config.OAuth2.GrantTypes, "client_credentials") + } + } } func pprofHandler(router *http.ServeMux) { @@ -563,3 +660,182 @@ func pprofHandler(router *http.ServeMux) { router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) router.HandleFunc("/debug/pprof/trace", pprof.Trace) } + +// newTLSReloader returns a [tls.Config] with GetCertificate or GetConfigForClient set +// to reload certificates from the given paths on SIGHUP or on file creates (atomic update via rename). +func newTLSReloader(logger *slog.Logger, certFile, keyFile, caFile string, baseConfig *tls.Config) (*tls.Config, error) { + // trigger reload on channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, syscall.SIGHUP) + + // files to watch + watchFiles := map[string]struct{}{ + certFile: {}, + keyFile: {}, + } + if caFile != "" { + watchFiles[caFile] = struct{}{} + } + watchDirs := make(map[string]struct{}) // dedupe dirs + for f := range watchFiles { + dir := filepath.Dir(f) + if !strings.HasPrefix(f, dir) { + // normalize name to have ./ prefix if only a local path was provided + // can't pass "" to watcher.Add + watchFiles[dir+string(filepath.Separator)+f] = struct{}{} + } + watchDirs[dir] = struct{}{} + } + // trigger reload on file change + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, fmt.Errorf("create watcher for TLS reloader: %v", err) + } + // recommended by fsnotify: watch the dir to handle renames + // https://pkg.go.dev/github.com/fsnotify/fsnotify#hdr-Watching_files + for dir := range watchDirs { + logger.Debug("watching dir", "dir", dir) + err := watcher.Add(dir) + if err != nil { + return nil, fmt.Errorf("watch dir for TLS reloader: %v", err) + } + } + + // load once outside the goroutine so we can return an error on misconfig + initialConfig, err := loadTLSConfig(certFile, keyFile, caFile, baseConfig) + if err != nil { + return nil, fmt.Errorf("load TLS config: %v", err) + } + + // stored version of current tls config + ptr := &atomic.Pointer[tls.Config]{} + ptr.Store(initialConfig) + + // start background worker to reload certs + go func() { + loop: + for { + select { + case sig := <-sigc: + logger.Debug("reloading cert from signal", "signal", sig) + case evt := <-watcher.Events: + if _, ok := watchFiles[evt.Name]; !ok || !evt.Has(fsnotify.Create) { + continue loop + } + logger.Debug("reloading cert from fsnotify", "event", evt.Name, "operation", evt.Op.String()) + case err := <-watcher.Errors: + logger.Error("TLS reloader watch", "err", err) + } + + loaded, err := loadTLSConfig(certFile, keyFile, caFile, baseConfig) + if err != nil { + logger.Error("reload TLS config", "err", err) + } + ptr.Store(loaded) + } + }() + + // https://pkg.go.dev/crypto/tls#baseConfig + // Server configurations must set one of Certificates, GetCertificate or GetConfigForClient. + if caFile != "" { + // grpc will use this via tls.Server for mTLS + initialConfig.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) { return ptr.Load(), nil } + } else { + // net/http only uses Certificates or GetCertificate + initialConfig.GetCertificate = func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { return &ptr.Load().Certificates[0], nil } + } + return initialConfig, nil +} + +// loadTLSConfig loads the given file paths into a [tls.Config] +func loadTLSConfig(certFile, keyFile, caFile string, baseConfig *tls.Config) (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("loading TLS keypair: %v", err) + } + loadedConfig := baseConfig.Clone() // copy + loadedConfig.Certificates = []tls.Certificate{cert} + if caFile != "" { + cPool := x509.NewCertPool() + clientCert, err := os.ReadFile(caFile) + if err != nil { + return nil, fmt.Errorf("reading from client CA file: %v", err) + } + if !cPool.AppendCertsFromPEM(clientCert) { + return nil, errors.New("failed to parse client CA") + } + + loadedConfig.ClientAuth = tls.RequireAndVerifyClientCert + loadedConfig.ClientCAs = cPool + } + return loadedConfig, nil +} + +// recordBuildInfo publishes information about Dex version and runtime info through an info metric (gauge). +func recordBuildInfo() { + buildInfo.WithLabelValues(version, runtime.Version(), fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)).Set(1) +} + +func parseSessionConfig(s *Sessions) (*server.SessionConfig, error) { + sc := &server.SessionConfig{ + CookieName: "dex_session", + AbsoluteLifetime: 24 * time.Hour, + ValidIfNotUsedFor: 1 * time.Hour, + RememberMeCheckedByDefault: true, + } + if s != nil { + if s.CookieName != "" { + sc.CookieName = s.CookieName + } + if s.AbsoluteLifetime != "" { + d, err := time.ParseDuration(s.AbsoluteLifetime) + if err != nil { + return nil, fmt.Errorf("invalid absoluteLifetime %q: %v", s.AbsoluteLifetime, err) + } + sc.AbsoluteLifetime = d + } + if s.ValidIfNotUsedFor != "" { + d, err := time.ParseDuration(s.ValidIfNotUsedFor) + if err != nil { + return nil, fmt.Errorf("invalid validIfNotUsedFor %q: %v", s.ValidIfNotUsedFor, err) + } + sc.ValidIfNotUsedFor = d + } + if s.RememberMeCheckedByDefault != nil { + sc.RememberMeCheckedByDefault = *s.RememberMeCheckedByDefault + } + } + if sc.AbsoluteLifetime <= 0 { + return nil, fmt.Errorf("absoluteLifetime must be positive, got %v", sc.AbsoluteLifetime) + } + if sc.ValidIfNotUsedFor <= 0 { + return nil, fmt.Errorf("validIfNotUsedFor must be positive, got %v", sc.ValidIfNotUsedFor) + } + if sc.ValidIfNotUsedFor > sc.AbsoluteLifetime { + return nil, fmt.Errorf("validIfNotUsedFor (%v) must not exceed absoluteLifetime (%v)", sc.ValidIfNotUsedFor, sc.AbsoluteLifetime) + } + return sc, nil +} + +func buildMFAProviders(authenticators []MFAAuthenticator, logger *slog.Logger) map[string]server.MFAProvider { + if len(authenticators) == 0 { + return nil + } + + providers := make(map[string]server.MFAProvider, len(authenticators)) + for _, auth := range authenticators { + switch auth.Type { + case "TOTP": + var cfg TOTPConfig + if err := json.Unmarshal(auth.Config, &cfg); err != nil { + logger.Error("failed to parse TOTP config", "id", auth.ID, "err", err) + continue + } + providers[auth.ID] = server.NewTOTPProvider(cfg.Issuer, auth.ConnectorTypes) + logger.Info("MFA authenticator configured", "id", auth.ID, "type", auth.Type) + default: + logger.Error("unknown MFA authenticator type, skipping", "id", auth.ID, "type", auth.Type) + } + } + return providers +} diff --git a/cmd/dex/serve_test.go b/cmd/dex/serve_test.go new file mode 100644 index 0000000000..12d0c0fff4 --- /dev/null +++ b/cmd/dex/serve_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewLogger(t *testing.T) { + t.Run("JSON", func(t *testing.T) { + logger, err := newLogger(slog.LevelInfo, "json", nil) + require.NoError(t, err) + require.NotEqual(t, (*slog.Logger)(nil), logger) + }) + + t.Run("Text", func(t *testing.T) { + logger, err := newLogger(slog.LevelError, "text", nil) + require.NoError(t, err) + require.NotEqual(t, (*slog.Logger)(nil), logger) + }) + + t.Run("Unknown", func(t *testing.T) { + logger, err := newLogger(slog.LevelError, "gofmt", nil) + require.Error(t, err) + require.Equal(t, "log format is not one of the supported values (json, text): gofmt", err.Error()) + require.Equal(t, (*slog.Logger)(nil), logger) + }) +} diff --git a/cmd/docker-entrypoint/main.go b/cmd/docker-entrypoint/main.go index 0c507d1712..14d837e5ee 100644 --- a/cmd/docker-entrypoint/main.go +++ b/cmd/docker-entrypoint/main.go @@ -17,21 +17,18 @@ func main() { // Note that this docker-entrypoint program is args[0], and it is provided with the true process // args. args := os.Args[1:] + if len(args) == 0 { + fmt.Println("error: no args passed to entrypoint") + os.Exit(1) + } - if err := run(args, realExec, realWhich); err != nil { + if err := run(args, realExec, realWhich, realGomplate); err != nil { fmt.Println("error:", err.Error()) os.Exit(1) } } -func realExec(fork bool, args ...string) error { - if fork { - if output, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { - return fmt.Errorf("cannot fork/exec command %s: %w (output: %q)", args, err, string(output)) - } - return nil - } - +func realExec(args ...string) error { argv0, err := exec.LookPath(args[0]) if err != nil { return fmt.Errorf("cannot lookup path for command %s: %w", args[0], err) @@ -52,34 +49,49 @@ func realWhich(path string) string { return fullPath } -func run(args []string, execFunc func(bool, ...string) error, whichFunc func(string) string) error { +func realGomplate(path string) (string, error) { + tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*") + if err != nil { + return "", fmt.Errorf("cannot create temp file: %w", err) + } + + cmd := exec.Command("gomplate", "-f", path, "-o", tmpFile.Name()) + // TODO(nabokihms): Workaround to run gomplate from a non-root directory in distroless images + // gomplate tries to access CWD on start, see: https://github.com/hairyhenderson/gomplate/pull/2202 + cmd.Dir = "/etc/dex" + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("error executing gomplate: %w, (output: %q)", err, string(output)) + } + + return tmpFile.Name(), nil +} + +func run(args []string, execFunc func(...string) error, whichFunc func(string) string, gomplateFunc func(string) (string, error)) error { if args[0] != "dex" && args[0] != whichFunc("dex") { - return execFunc(false, args...) + return execFunc(args...) } if args[1] != "serve" { - return execFunc(false, args...) + return execFunc(args...) } newArgs := []string{} for _, tplCandidate := range args { if hasSuffixes(tplCandidate, ".tpl", ".tmpl", ".yaml") { - tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*") + fileName, err := gomplateFunc(tplCandidate) if err != nil { - return fmt.Errorf("cannot create temp file: %w", err) - } - - if err := execFunc(true, "gomplate", "-f", tplCandidate, "-o", tmpFile.Name()); err != nil { return err } - newArgs = append(newArgs, tmpFile.Name()) + newArgs = append(newArgs, fileName) } else { newArgs = append(newArgs, tplCandidate) } } - return execFunc(false, newArgs...) + return execFunc(newArgs...) } func hasSuffixes(s string, suffixes ...string) bool { diff --git a/cmd/docker-entrypoint/main_test.go b/cmd/docker-entrypoint/main_test.go index c8aef16979..49da3b5f02 100644 --- a/cmd/docker-entrypoint/main_test.go +++ b/cmd/docker-entrypoint/main_test.go @@ -6,7 +6,7 @@ import ( ) type execArgs struct { - fork bool + gomplate bool argPrefixes []string } @@ -16,98 +16,89 @@ func TestRun(t *testing.T) { args []string execReturns error whichReturns string - wantExecArgs []execArgs + wantExecArgs execArgs wantErr error }{ { name: "executable not dex", args: []string{"tuna", "fish"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"tuna", "fish"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"tuna", "fish"}}, }, { name: "executable is full path to dex", args: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}, whichReturns: "/usr/local/bin/dex", - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}, }, { name: "command is not serve", args: []string{"dex", "marshmallow", "zelda"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}, }, { name: "no templates", args: []string{"dex", "serve", "config.yaml.not-a-template"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}, }, { name: "no templates", args: []string{"dex", "serve", "config.yaml.not-a-template"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}, }, { - name: ".tpl template", - args: []string{"dex", "serve", "config.tpl"}, - wantExecArgs: []execArgs{ - {fork: true, argPrefixes: []string{"gomplate", "-f", "config.tpl", "-o", "/tmp/dex.config.yaml-"}}, - {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, - }, + name: ".tpl template", + args: []string{"dex", "serve", "config.tpl"}, + wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, }, { - name: ".tmpl template", - args: []string{"dex", "serve", "config.tmpl"}, - wantExecArgs: []execArgs{ - {fork: true, argPrefixes: []string{"gomplate", "-f", "config.tmpl", "-o", "/tmp/dex.config.yaml-"}}, - {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, - }, + name: ".tmpl template", + args: []string{"dex", "serve", "config.tmpl"}, + wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, }, { - name: ".yaml template", - args: []string{"dex", "serve", "some/path/config.yaml"}, - wantExecArgs: []execArgs{ - {fork: true, argPrefixes: []string{"gomplate", "-f", "some/path/config.yaml", "-o", "/tmp/dex.config.yaml-"}}, - {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, - }, + name: ".yaml template", + args: []string{"dex", "serve", "some/path/config.yaml"}, + wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - var gotExecForks []bool - var gotExecArgs [][]string - fakeExec := func(fork bool, args ...string) error { - gotExecForks = append(gotExecForks, fork) - gotExecArgs = append(gotExecArgs, args) + var gotExecArgs []string + var runsGomplate bool + + fakeExec := func(args ...string) error { + gotExecArgs = append(args, gotExecArgs...) return test.execReturns } fakeWhich := func(_ string) string { return test.whichReturns } - gotErr := run(test.args, fakeExec, fakeWhich) + fakeGomplate := func(file string) (string, error) { + runsGomplate = true + return "/tmp/dex.config.yaml-", nil + } + + gotErr := run(test.args, fakeExec, fakeWhich, fakeGomplate) if (test.wantErr == nil) != (gotErr == nil) { t.Errorf("wanted error %s, got %s", test.wantErr, gotErr) } - if !execArgsMatch(test.wantExecArgs, gotExecForks, gotExecArgs) { - t.Errorf("wanted exec args %+v, got %+v %+v", test.wantExecArgs, gotExecForks, gotExecArgs) + + if !execArgsMatch(test.wantExecArgs, runsGomplate, gotExecArgs) { + t.Errorf("wanted exec args %+v (running gomplate: %+v), got %+v (running gomplate: %+v)", + test.wantExecArgs.argPrefixes, test.wantExecArgs.gomplate, gotExecArgs, runsGomplate) } }) } } -func execArgsMatch(wantExecArgs []execArgs, gotForks []bool, gotExecArgs [][]string) bool { - if len(wantExecArgs) != len(gotForks) { +func execArgsMatch(wantExecArgs execArgs, gomplate bool, gotExecArgs []string) bool { + if wantExecArgs.gomplate != gomplate { return false } - - for i := range wantExecArgs { - if wantExecArgs[i].fork != gotForks[i] { + for i := range wantExecArgs.argPrefixes { + if !strings.HasPrefix(gotExecArgs[i], wantExecArgs.argPrefixes[i]) { return false } - for j := range wantExecArgs[i].argPrefixes { - if !strings.HasPrefix(gotExecArgs[i][j], wantExecArgs[i].argPrefixes[j]) { - return false - } - } } - return true } diff --git a/config.dev.yaml b/config.dev.yaml index dda65e08f7..bdbc2718d3 100644 --- a/config.dev.yaml +++ b/config.dev.yaml @@ -32,4 +32,10 @@ staticPasswords: - email: "admin@example.com" hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" + name: "Admin User" + emailVerified: true + preferredUsername: "admin" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/config.yaml.dist b/config.yaml.dist index ba7bad68e0..917f8d1f5f 100644 --- a/config.yaml.dist +++ b/config.yaml.dist @@ -55,6 +55,8 @@ web: # https: 127.0.0.1:5554 # tlsCert: /etc/dex/tls.crt # tlsKey: /etc/dex/tls.key + # tlsMinVersion: 1.2 + # tlsMaxVersion: 1.3 # Dex UI configuration # frontend: @@ -70,6 +72,8 @@ web: # logger: # level: "debug" # format: "text" # can also be "json" +# # Drop these attribute keys from all log output (useful for GDPR/PII suppression). +# # excludeFields: [email, username, preferred_username, groups] # gRPC API configuration # Uncomment this block to enable the gRPC API. @@ -107,6 +111,13 @@ web: # # # Uncomment to use a specific connector for password grants # passwordConnector: local +# +# # PKCE (Proof Key for Code Exchange) configuration +# pkce: +# # If true, PKCE is required for all authorization code flows (OAuth 2.1). +# enforce: false +# # Supported code challenge methods. Defaults to ["S256", "plain"]. +# codeChallengeMethodsSupported: ["S256", "plain"] # Static clients registered in Dex by default. # @@ -117,8 +128,33 @@ web: # - 'http://127.0.0.1:5555/callback' # name: 'Example App' # secret: ZXhhbXBsZS1hcHAtc2VjcmV0 +# +# # Example using environment variables +# # These fields are mutually exclusive with id and secret respectively. +# - idEnv: DEX_CLIENT_ID +# secretEnv: DEX_CLIENT_SECRET +# redirectURIs: +# - 'https://app.example.com/callback' +# name: 'Production App' +# +# # Example of a public client (no secret required) +# - id: example-device-client +# redirectURIs: +# - /device/callback +# name: 'Static Client for Device Flow' +# public: true +# +# # Example of a client restricted to specific connectors +# - id: restricted-client +# secret: restricted-client-secret +# redirectURIs: +# - 'https://app.example.com/callback' +# name: 'Restricted Client' +# allowedConnectors: +# - github +# - google -# Connectors are used to authenticate users agains upstream identity providers. +# Connectors are used to authenticate users against upstream identity providers. # # See the documentation (https://dexidp.io/docs/connectors/) for further information. # connectors: [] @@ -133,4 +169,19 @@ enablePasswordDB: true # A static list of passwords for the password connector. # # Alternatively, passwords my be added/updated through the gRPC API. -# staticPasswords: [] +# staticPasswords: +# - email: "user@example.com" +# # bcrypt hash of the string "password" +# hash: "$2a$10$examplehash..." +# username: "user-login" +# # Optional. Maps to OIDC "name" claim. Defaults to username. +# name: "User Full Name" +# # Optional. Maps to OIDC "email_verified" claim. Defaults to true. +# emailVerified: true +# # Optional. Maps to OIDC "preferred_username" claim. +# preferredUsername: "user-public" +# # Optional. Maps to OIDC "groups" claim (when 'groups' scope is requested). +# groups: +# - "team-a" +# - "team-a/admins" +# userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/connector/atlassiancrowd/atlassiancrowd.go b/connector/atlassiancrowd/atlassiancrowd.go index e2ca94b0de..ca92214785 100644 --- a/connector/atlassiancrowd/atlassiancrowd.go +++ b/connector/atlassiancrowd/atlassiancrowd.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net" "net/http" "strings" @@ -14,7 +15,6 @@ import ( "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" ) // Config holds configuration options for Atlassian Crowd connector. @@ -24,18 +24,17 @@ import ( // // An example config: // -// type: atlassian-crowd -// config: -// baseURL: https://crowd.example.com/context -// clientID: applogin -// clientSecret: appP4$$w0rd -// # users can be restricted by a list of groups -// groups: -// - admin -// # Prompt for username field -// usernamePrompt: Login -// preferredUsernameField: name -// +// type: atlassian-crowd +// config: +// baseURL: https://crowd.example.com/context +// clientID: applogin +// clientSecret: appP4$$w0rd +// # users can be restricted by a list of groups +// groups: +// - admin +// # Prompt for username field +// usernamePrompt: Login +// preferredUsernameField: name type Config struct { BaseURL string `json:"baseURL"` ClientID string `json:"clientID"` @@ -81,16 +80,11 @@ type crowdAuthenticationError struct { } // Open returns a strategy for logging in through Atlassian Crowd -func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { if c.BaseURL == "" { return nil, fmt.Errorf("crowd: no baseURL provided for crowd connector") } - return &crowdConnector{Config: *c, logger: logger}, nil -} - -type crowdConnector struct { - Config - logger log.Logger + return &crowdConnector{Config: *c, logger: logger.With(slog.Group("connector", "type", "atlassiancrowd", "id", id))}, nil } var ( @@ -98,6 +92,11 @@ var ( _ connector.RefreshConnector = (*crowdConnector)(nil) ) +type crowdConnector struct { + Config + logger *slog.Logger +} + type refreshData struct { Username string `json:"username"` } @@ -376,7 +375,7 @@ func (c *crowdConnector) identityFromCrowdUser(user crowdUser) connector.Identit identity.PreferredUsername = user.Email default: if c.PreferredUsernameField != "" { - c.logger.Warnf("preferred_username left empty. Invalid crowd field mapped to preferred_username: %s", c.PreferredUsernameField) + c.logger.Warn("preferred_username left empty. Invalid crowd field mapped to preferred_username", "field", c.PreferredUsernameField) } } @@ -437,12 +436,12 @@ func (c *crowdConnector) validateCrowdResponse(resp *http.Response) ([]byte, err } if resp.StatusCode == http.StatusForbidden && strings.Contains(string(body), "The server understood the request but refuses to authorize it.") { - c.logger.Debugf("crowd response validation failed: %s", string(body)) + c.logger.Debug("crowd response validation failed", "response", string(body)) return nil, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", c.BaseURL) } if resp.StatusCode == http.StatusUnauthorized && string(body) == "Application failed to authenticate" { - c.logger.Debugf("crowd response validation failed: %s", string(body)) + c.logger.Debug("crowd response validation failed", "response", string(body)) return nil, fmt.Errorf("dex failed to authenticate Crowd Application with ID %q", c.ClientID) } return body, nil diff --git a/connector/atlassiancrowd/atlassiancrowd_test.go b/connector/atlassiancrowd/atlassiancrowd_test.go index 36789a3919..17d0422ac8 100644 --- a/connector/atlassiancrowd/atlassiancrowd_test.go +++ b/connector/atlassiancrowd/atlassiancrowd_test.go @@ -6,13 +6,11 @@ import ( "crypto/tls" "encoding/json" "fmt" - "io" + "log/slog" "net/http" "net/http/httptest" "reflect" "testing" - - "github.com/sirupsen/logrus" ) func TestUserGroups(t *testing.T) { @@ -115,7 +113,7 @@ func TestIdentityFromCrowdUser(t *testing.T) { expectEquals(t, user.Name, "testuser") expectEquals(t, user.Email, "testuser@example.com") - // Test unconfigured behaviour + // Test unconfigured behavior i := c.identityFromCrowdUser(user) expectEquals(t, i.UserID, "12345") expectEquals(t, i.Username, "testuser") @@ -151,11 +149,7 @@ type TestServerResponse struct { func newTestCrowdConnector(baseURL string) crowdConnector { connector := crowdConnector{} connector.BaseURL = baseURL - connector.logger = &logrus.Logger{ - Out: io.Discard, - Level: logrus.DebugLevel, - Formatter: &logrus.TextFormatter{DisableColors: true}, - } + connector.logger = slog.New(slog.DiscardHandler) return connector } diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go index 8715412146..5756a0d401 100644 --- a/connector/authproxy/authproxy.go +++ b/connector/authproxy/authproxy.go @@ -5,12 +5,12 @@ package authproxy import ( "fmt" + "log/slog" "net/http" "net/url" "strings" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" ) // Config holds the configuration parameters for a connector which returns an @@ -19,67 +19,117 @@ import ( // Headers retrieved to fetch user's email and group can be configured // with userHeader and groupHeader. type Config struct { - UserHeader string `json:"userHeader"` - GroupHeader string `json:"groupHeader"` - Groups []string `json:"staticGroups"` + UserIDHeader string `json:"userIDHeader"` + UserHeader string `json:"userHeader"` + UserNameHeader string `json:"userNameHeader"` + EmailHeader string `json:"emailHeader"` + GroupHeader string `json:"groupHeader"` + GroupHeaderSeparator string `json:"groupHeaderSeparator"` + Groups []string `json:"staticGroups"` } // Open returns an authentication strategy which requires no user interaction. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { + userIDHeader := c.UserIDHeader + if userIDHeader == "" { + userIDHeader = "X-Remote-User-Id" + } userHeader := c.UserHeader if userHeader == "" { userHeader = "X-Remote-User" } + userNameHeader := c.UserNameHeader + if userNameHeader == "" { + userNameHeader = "X-Remote-User-Name" + } + emailHeader := c.EmailHeader + if emailHeader == "" { + emailHeader = "X-Remote-User-Email" + } groupHeader := c.GroupHeader if groupHeader == "" { groupHeader = "X-Remote-Group" } + groupHeaderSeparator := c.GroupHeaderSeparator + if groupHeaderSeparator == "" { + groupHeaderSeparator = "," + } - return &callback{userHeader: userHeader, groupHeader: groupHeader, logger: logger, pathSuffix: "/" + id, groups: c.Groups}, nil + return &callback{ + userIDHeader: userIDHeader, + userHeader: userHeader, + userNameHeader: userNameHeader, + emailHeader: emailHeader, + groupHeader: groupHeader, + groupHeaderSeparator: groupHeaderSeparator, + groups: c.Groups, + logger: logger.With(slog.Group("connector", "type", "authproxy", "id", id)), + pathSuffix: "/" + id, + }, nil } +var _ connector.CallbackConnector = (*callback)(nil) + // Callback is a connector which returns an identity with the HTTP header // X-Remote-User as verified email. type callback struct { - userHeader string - groupHeader string - groups []string - logger log.Logger - pathSuffix string + userIDHeader string + userNameHeader string + userHeader string + emailHeader string + groupHeader string + groupHeaderSeparator string + groups []string + logger *slog.Logger + pathSuffix string } // LoginURL returns the URL to redirect the user to login with. -func (m *callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { +func (m *callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, []byte, error) { u, err := url.Parse(callbackURL) if err != nil { - return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) + return "", nil, fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) } u.Path += m.pathSuffix v := u.Query() v.Set("state", state) u.RawQuery = v.Encode() - return u.String(), nil + return u.String(), nil, nil } // HandleCallback parses the request and returns the user's identity -func (m *callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) { +func (m *callback) HandleCallback(s connector.Scopes, _ []byte, r *http.Request) (connector.Identity, error) { remoteUser := r.Header.Get(m.userHeader) if remoteUser == "" { return connector.Identity{}, fmt.Errorf("required HTTP header %s is not set", m.userHeader) } + remoteUserName := r.Header.Get(m.userNameHeader) + if remoteUserName == "" { + remoteUserName = remoteUser + } + remoteUserID := r.Header.Get(m.userIDHeader) + if remoteUserID == "" { + remoteUserID = remoteUser + } + remoteUserEmail := r.Header.Get(m.emailHeader) + if remoteUserEmail == "" { + remoteUserEmail = remoteUser + } groups := m.groups headerGroup := r.Header.Get(m.groupHeader) if headerGroup != "" { - splitheaderGroup := strings.Split(headerGroup, ",") + splitheaderGroup := strings.Split(headerGroup, m.groupHeaderSeparator) for i, v := range splitheaderGroup { splitheaderGroup[i] = strings.TrimSpace(v) } groups = append(splitheaderGroup, groups...) } return connector.Identity{ - UserID: remoteUser, // TODO: figure out if this is a bad ID value. - Email: remoteUser, - EmailVerified: true, - Groups: groups, + UserID: remoteUserID, + Username: remoteUser, + PreferredUsername: remoteUserName, + Email: remoteUserEmail, + EmailVerified: true, + Groups: groups, }, nil } diff --git a/connector/authproxy/authproxy_test.go b/connector/authproxy/authproxy_test.go index 5d42530e07..bd8b4f3671 100644 --- a/connector/authproxy/authproxy_test.go +++ b/connector/authproxy/authproxy_test.go @@ -1,55 +1,82 @@ package authproxy import ( - "io" + "log/slog" "net/http" "reflect" "testing" - "github.com/sirupsen/logrus" - "github.com/dexidp/dex/connector" ) const ( - testEmail = "testuser@example.com" - testGroup1 = "group1" - testGroup2 = "group2" - testGroup3 = "group 3" - testGroup4 = "group 4" - testStaticGroup1 = "static1" - testStaticGroup2 = "static 2" + testEmail = "testuser@example.com" + testGroup1 = "group1" + testGroup2 = "group2" + testGroup3 = "group 3" + testGroup4 = "group 4" + testStaticGroup1 = "static1" + testStaticGroup2 = "static 2" + testUsername = "Test User" + testPreferredUsername = "testuser" + testUserID = "1234567890" ) -var logger = &logrus.Logger{Out: io.Discard, Formatter: &logrus.TextFormatter{}} +var logger = slog.New(slog.DiscardHandler) func TestUser(t *testing.T) { - config := Config{ - UserHeader: "X-Remote-User", + config := Config{} + + conn, _ := config.Open("test", logger) + callback := conn.(*callback) + + req, err := http.NewRequest("GET", "/", nil) + expectNil(t, err) + req.Header = map[string][]string{ + "X-Remote-User": {testUsername}, } - conn := callback{userHeader: config.UserHeader, logger: logger, pathSuffix: "/test"} + + ident, err := callback.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, nil, req) + expectNil(t, err) + + // If not specified, the userID and email should fall back to the remote user + expectEquals(t, ident.UserID, testUsername) + expectEquals(t, ident.PreferredUsername, testUsername) + expectEquals(t, ident.Username, testUsername) + expectEquals(t, ident.Email, testUsername) + expectEquals(t, len(ident.Groups), 0) +} + +func TestExtraHeaders(t *testing.T) { + config := Config{} + + conn, _ := config.Open("test", logger) + callback := conn.(*callback) req, err := http.NewRequest("GET", "/", nil) expectNil(t, err) req.Header = map[string][]string{ - "X-Remote-User": {testEmail}, + "X-Remote-User-Id": {testUserID}, + "X-Remote-User": {testUsername}, + "X-Remote-User-Name": {testPreferredUsername}, + "X-Remote-User-Email": {testEmail}, } - ident, err := conn.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, req) + ident, err := callback.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, nil, req) expectNil(t, err) - expectEquals(t, ident.UserID, testEmail) + expectEquals(t, ident.UserID, testUserID) + expectEquals(t, ident.PreferredUsername, testPreferredUsername) + expectEquals(t, ident.Username, testUsername) expectEquals(t, ident.Email, testEmail) expectEquals(t, len(ident.Groups), 0) } func TestSingleGroup(t *testing.T) { - config := Config{ - UserHeader: "X-Remote-User", - GroupHeader: "X-Remote-Group", - } + config := Config{} - conn := callback{userHeader: config.UserHeader, groupHeader: config.GroupHeader, logger: logger, pathSuffix: "/test"} + conn, _ := config.Open("test", logger) + callback := conn.(*callback) req, err := http.NewRequest("GET", "/", nil) expectNil(t, err) @@ -58,7 +85,7 @@ func TestSingleGroup(t *testing.T) { "X-Remote-Group": {testGroup1}, } - ident, err := conn.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, req) + ident, err := callback.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, nil, req) expectNil(t, err) expectEquals(t, ident.UserID, testEmail) @@ -67,21 +94,45 @@ func TestSingleGroup(t *testing.T) { } func TestMultipleGroup(t *testing.T) { + config := Config{} + + conn, _ := config.Open("test", logger) + callback := conn.(*callback) + + req, err := http.NewRequest("GET", "/", nil) + expectNil(t, err) + req.Header = map[string][]string{ + "X-Remote-User": {testEmail}, + "X-Remote-Group": {testGroup1 + ", " + testGroup2 + ", " + testGroup3 + ", " + testGroup4}, + } + + ident, err := callback.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, nil, req) + expectNil(t, err) + + expectEquals(t, ident.UserID, testEmail) + expectEquals(t, len(ident.Groups), 4) + expectEquals(t, ident.Groups[0], testGroup1) + expectEquals(t, ident.Groups[1], testGroup2) + expectEquals(t, ident.Groups[2], testGroup3) + expectEquals(t, ident.Groups[3], testGroup4) +} + +func TestMultipleGroupWithCustomSeparator(t *testing.T) { config := Config{ - UserHeader: "X-Remote-User", - GroupHeader: "X-Remote-Group", + GroupHeaderSeparator: ";", } - conn := callback{userHeader: config.UserHeader, groupHeader: config.GroupHeader, logger: logger, pathSuffix: "/test"} + conn, _ := config.Open("test", logger) + callback := conn.(*callback) req, err := http.NewRequest("GET", "/", nil) expectNil(t, err) req.Header = map[string][]string{ "X-Remote-User": {testEmail}, - "X-Remote-Group": {testGroup1 + ", " + testGroup2 + ", " + testGroup3 + ", " + testGroup4}, + "X-Remote-Group": {testGroup1 + ";" + testGroup2 + ";" + testGroup3 + ";" + testGroup4}, } - ident, err := conn.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, req) + ident, err := callback.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, nil, req) expectNil(t, err) expectEquals(t, ident.UserID, testEmail) @@ -94,12 +145,11 @@ func TestMultipleGroup(t *testing.T) { func TestStaticGroup(t *testing.T) { config := Config{ - UserHeader: "X-Remote-User", - GroupHeader: "X-Remote-Group", - Groups: []string{"static1", "static 2"}, + Groups: []string{"static1", "static 2"}, } - conn := callback{userHeader: config.UserHeader, groupHeader: config.GroupHeader, groups: config.Groups, logger: logger, pathSuffix: "/test"} + conn, _ := config.Open("test", logger) + callback := conn.(*callback) req, err := http.NewRequest("GET", "/", nil) expectNil(t, err) @@ -108,7 +158,7 @@ func TestStaticGroup(t *testing.T) { "X-Remote-Group": {testGroup1 + ", " + testGroup2 + ", " + testGroup3 + ", " + testGroup4}, } - ident, err := conn.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, req) + ident, err := callback.HandleCallback(connector.Scopes{OfflineAccess: true, Groups: true}, nil, req) expectNil(t, err) expectEquals(t, ident.UserID, testEmail) diff --git a/connector/bitbucketcloud/bitbucketcloud.go b/connector/bitbucketcloud/bitbucketcloud.go index 27eafb5299..d7fb64caa6 100644 --- a/connector/bitbucketcloud/bitbucketcloud.go +++ b/connector/bitbucketcloud/bitbucketcloud.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "sync" "time" @@ -16,7 +17,6 @@ import ( "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" ) const ( @@ -42,7 +42,7 @@ type Config struct { } // Open returns a strategy for logging in through Bitbucket. -func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { b := bitbucketConnector{ redirectURI: c.RedirectURI, teams: c.Teams, @@ -51,7 +51,7 @@ func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) includeTeamGroups: c.IncludeTeamGroups, apiURL: apiURL, legacyAPIURL: legacyAPIURL, - logger: logger, + logger: logger.With(slog.Group("connector", "type", "bitbucketcloud", "id", id)), } return &b, nil @@ -73,7 +73,7 @@ type bitbucketConnector struct { teams []string clientID string clientSecret string - logger log.Logger + logger *slog.Logger apiURL string legacyAPIURL string @@ -111,12 +111,12 @@ func (b *bitbucketConnector) oauth2Config(scopes connector.Scopes) *oauth2.Confi } } -func (b *bitbucketConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (b *bitbucketConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if b.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, b.redirectURI) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, b.redirectURI) } - return b.oauth2Config(scopes).AuthCodeURL(state), nil + return b.oauth2Config(scopes).AuthCodeURL(state), nil, nil } type oauth2Error struct { @@ -131,7 +131,7 @@ func (e *oauth2Error) Error() string { return e.error + ": " + e.errorDescription } -func (b *bitbucketConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (b *bitbucketConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} diff --git a/connector/bitbucketcloud/bitbucketcloud_test.go b/connector/bitbucketcloud/bitbucketcloud_test.go index 9545ff09c5..67a74dab38 100644 --- a/connector/bitbucketcloud/bitbucketcloud_test.go +++ b/connector/bitbucketcloud/bitbucketcloud_test.go @@ -102,7 +102,7 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) { expectNil(t, err) bitbucketConnector := bitbucketConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: newClient()} - identity, err := bitbucketConnector.HandleCallback(connector.Scopes{}, req) + identity, err := bitbucketConnector.HandleCallback(connector.Scopes{}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some-login") diff --git a/connector/connector.go b/connector/connector.go index aab994b468..1e5547896c 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -3,9 +3,22 @@ package connector import ( "context" + "fmt" "net/http" ) +// UserNotInRequiredGroupsError is returned by a connector when a user +// successfully authenticates but is not a member of any of the required groups. +// The server will respond with HTTP 403 Forbidden instead of 500. +type UserNotInRequiredGroupsError struct { + UserID string + Groups []string +} + +func (e *UserNotInRequiredGroupsError) Error() string { + return fmt.Sprintf("user %q is not in any of the required groups %v", e.UserID, e.Groups) +} + // Connector is a mechanism for federating login to a remote identity service. // // Implementations are expected to implement either the PasswordConnector or @@ -63,14 +76,15 @@ type CallbackConnector interface { // requested if one has already been issues. There's no good general answer // for these kind of restrictions, and may require this package to become more // aware of the global set of user/connector interactions. - LoginURL(s Scopes, callbackURL, state string) (string, error) + LoginURL(s Scopes, callbackURL, state string) (string, []byte, error) // Handle the callback to the server and return an identity. - HandleCallback(s Scopes, r *http.Request) (identity Identity, err error) + HandleCallback(s Scopes, connData []byte, r *http.Request) (identity Identity, err error) } // SAMLConnector represents SAML connectors which implement the HTTP POST binding. -// RelayState is handled by the server. +// +// RelayState is handled by the server. // // See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf // "3.5 HTTP POST Binding" @@ -98,3 +112,7 @@ type RefreshConnector interface { // changes since the token was last refreshed. Refresh(ctx context.Context, s Scopes, identity Identity) (Identity, error) } + +type TokenIdentityConnector interface { + TokenIdentity(ctx context.Context, subjectTokenType, subjectToken string) (Identity, error) +} diff --git a/connector/gitea/gitea.go b/connector/gitea/gitea.go index 6b02099414..059c861705 100644 --- a/connector/gitea/gitea.go +++ b/connector/gitea/gitea.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "strconv" "sync" @@ -15,7 +16,6 @@ import ( "golang.org/x/oauth2" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" ) // Config holds configuration options for gitea logins. @@ -51,7 +51,7 @@ type giteaUser struct { } // Open returns a strategy for logging in through Gitea -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { if c.BaseURL == "" { c.BaseURL = "https://gitea.com" } @@ -61,7 +61,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) orgs: c.Orgs, clientID: c.ClientID, clientSecret: c.ClientSecret, - logger: logger, + logger: logger.With(slog.Group("connector", "type", "gitea", "id", id)), loadAllGroups: c.LoadAllGroups, useLoginAsID: c.UseLoginAsID, }, nil @@ -84,7 +84,7 @@ type giteaConnector struct { orgs []Org clientID string clientSecret string - logger log.Logger + logger *slog.Logger httpClient *http.Client // if set to true and no orgs are configured then connector loads all user claims (all orgs and team) loadAllGroups bool @@ -102,11 +102,11 @@ func (c *giteaConnector) oauth2Config(_ connector.Scopes) *oauth2.Config { } } -func (c *giteaConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *giteaConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", c.redirectURI, callbackURL) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", c.redirectURI, callbackURL) } - return c.oauth2Config(scopes).AuthCodeURL(state), nil + return c.oauth2Config(scopes).AuthCodeURL(state), nil, nil } type oauth2Error struct { @@ -121,7 +121,7 @@ func (e *oauth2Error) Error() string { return e.error + ": " + e.errorDescription } -func (c *giteaConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *giteaConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} diff --git a/connector/gitea/gitea_test.go b/connector/gitea/gitea_test.go index a71d79956e..4fe7768901 100644 --- a/connector/gitea/gitea_test.go +++ b/connector/gitea/gitea_test.go @@ -30,14 +30,14 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) { expectNil(t, err) c := giteaConnector{baseURL: s.URL, httpClient: newClient()} - identity, err := c.HandleCallback(connector.Scopes{}, req) + identity, err := c.HandleCallback(connector.Scopes{}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some@email.com") expectEquals(t, identity.UserID, "12345678") c = giteaConnector{baseURL: s.URL, httpClient: newClient()} - identity, err = c.HandleCallback(connector.Scopes{}, req) + identity, err = c.HandleCallback(connector.Scopes{}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some@email.com") diff --git a/connector/github/github.go b/connector/github/github.go index ef8d418fa8..0712d3b9ff 100644 --- a/connector/github/github.go +++ b/connector/github/github.go @@ -3,26 +3,22 @@ package github import ( "context" - "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" "io" - "net" + "log/slog" "net/http" - "os" "regexp" "strconv" "strings" - "time" "golang.org/x/oauth2" "golang.org/x/oauth2/github" "github.com/dexidp/dex/connector" groups_pkg "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/httpclient" ) const ( @@ -32,6 +28,8 @@ const ( // GitHub requires this scope to access '/user/teams' and '/orgs' API endpoints // which are used when a client includes the 'groups' scope. scopeOrgs = "read:org" + // githubAPIVersion pins the GitHub REST API version used in requests. + githubAPIVersion = "2022-11-28" ) // Pagination URL patterns @@ -43,16 +41,17 @@ var ( // Config holds configuration options for github logins. type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - Org string `json:"org"` - Orgs []Org `json:"orgs"` - HostName string `json:"hostName"` - RootCA string `json:"rootCA"` - TeamNameField string `json:"teamNameField"` - LoadAllGroups bool `json:"loadAllGroups"` - UseLoginAsID bool `json:"useLoginAsID"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + Org string `json:"org"` + Orgs []Org `json:"orgs"` + HostName string `json:"hostName"` + RootCA string `json:"rootCA"` + TeamNameField string `json:"teamNameField"` + LoadAllGroups bool `json:"loadAllGroups"` + UseLoginAsID bool `json:"useLoginAsID"` + PreferredEmailDomain string `json:"preferredEmailDomain"` } // Org holds org-team filters, in which teams are optional. @@ -69,7 +68,7 @@ type Org struct { } // Open returns a strategy for logging in through GitHub. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { if c.Org != "" { // Return error if both 'org' and 'orgs' fields are used. if len(c.Orgs) > 0 { @@ -79,14 +78,15 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) } g := githubConnector{ - redirectURI: c.RedirectURI, - org: c.Org, - orgs: c.Orgs, - clientID: c.ClientID, - clientSecret: c.ClientSecret, - apiURL: apiURL, - logger: logger, - useLoginAsID: c.UseLoginAsID, + redirectURI: c.RedirectURI, + org: c.Org, + orgs: c.Orgs, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + apiURL: apiURL, + logger: logger.With(slog.Group("connector", "type", "github", "id", id)), + useLoginAsID: c.UseLoginAsID, + preferredEmailDomain: c.PreferredEmailDomain, } if c.HostName != "" { @@ -106,7 +106,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) g.rootCA = c.RootCA var err error - if g.httpClient, err = newHTTPClient(g.rootCA); err != nil { + if g.httpClient, err = httpclient.NewHTTPClient([]string{g.rootCA}, false); err != nil { return nil, fmt.Errorf("failed to create HTTP client: %v", err) } } @@ -119,6 +119,12 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) return nil, fmt.Errorf("invalid connector config: unsupported team name field value `%s`", c.TeamNameField) } + if c.PreferredEmailDomain != "" { + if strings.HasSuffix(c.PreferredEmailDomain, "*") { + return nil, errors.New("invalid PreferredEmailDomain: glob pattern cannot end with \"*\"") + } + } + return &g, nil } @@ -138,7 +144,7 @@ type githubConnector struct { orgs []Org clientID string clientSecret string - logger log.Logger + logger *slog.Logger // apiURL defaults to "https://api.github.com" apiURL string // hostName of the GitHub enterprise account. @@ -153,6 +159,8 @@ type githubConnector struct { loadAllGroups bool // if set to true will use the user's handle rather than their numeric id as the ID useLoginAsID bool + // the domain to be preferred among the user's emails. e.g. "github.com" + preferredEmailDomain string } // groupsRequired returns whether dex requires GitHub's 'read:org' scope. Dex @@ -188,12 +196,12 @@ func (c *githubConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config { } } -func (c *githubConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *githubConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } - return c.oauth2Config(scopes).AuthCodeURL(state), nil + return c.oauth2Config(scopes).AuthCodeURL(state), nil, nil } type oauth2Error struct { @@ -208,35 +216,7 @@ func (e *oauth2Error) Error() string { return e.error + ": " + e.errorDescription } -// newHTTPClient returns a new HTTP client that trusts the custom declared rootCA cert. -func newHTTPClient(rootCA string) (*http.Client, error) { - tlsConfig := tls.Config{RootCAs: x509.NewCertPool()} - rootCABytes, err := os.ReadFile(rootCA) - if err != nil { - return nil, fmt.Errorf("failed to read root-ca: %v", err) - } - if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { - return nil, fmt.Errorf("no certs found in root CA file %q", rootCA) - } - - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tlsConfig, - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, nil -} - -func (c *githubConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *githubConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} @@ -356,9 +336,11 @@ func formatTeamName(org string, team string) string { // groupsForOrgs enforces org and team constraints on user authorization // Cases in which user is authorized: -// N orgs, no teams: user is member of at least 1 org -// N orgs, M teams per org: user is member of any team from at least 1 org -// N-1 orgs, M teams per org, 1 org with no teams: user is member of any team +// +// N orgs, no teams: user is member of at least 1 org +// N orgs, M teams per org: user is member of any team from at least 1 org +// N-1 orgs, M teams per org, 1 org with no teams: user is member of any team +// // from at least 1 org, or member of org with no teams func (c *githubConnector) groupsForOrgs(ctx context.Context, client *http.Client, userName string) ([]string, error) { groups := make([]string, 0) @@ -382,7 +364,7 @@ func (c *githubConnector) groupsForOrgs(ctx context.Context, client *http.Client if len(org.Teams) == 0 { inOrgNoTeams = true } else if teams = groups_pkg.Filter(teams, org.Teams); len(teams) == 0 { - c.logger.Infof("github: user %q in org %q but no teams", userName, org.Name) + c.logger.Info("user in org but no teams", "user", userName, "org", org.Name) } for _, teamName := range teams { @@ -482,6 +464,7 @@ func get(ctx context.Context, client *http.Client, apiURL string, v interface{}) return "", fmt.Errorf("github: new req: %v", err) } req = req.WithContext(ctx) + req.Header.Set("X-GitHub-Api-Version", githubAPIVersion) resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("github: get URL %v", err) @@ -551,9 +534,10 @@ func (c *githubConnector) user(ctx context.Context, client *http.Client) (user, return u, err } - // Only public user emails are returned by 'GET /user'. u.Email will be empty - // if a users' email is private. We must retrieve private emails explicitly. - if u.Email == "" { + // Only public user emails are returned by 'GET /user'. + // If a user has no public email, we must retrieve private emails explicitly. + // If preferredEmailDomain is set, we always need to retrieve all emails. + if u.Email == "" || c.preferredEmailDomain != "" { var err error if u.Email, err = c.userEmail(ctx, client); err != nil { return u, err @@ -578,7 +562,13 @@ type userEmail struct { // The HTTP client is expected to be constructed by the golang.org/x/oauth2 package, // which inserts a bearer token as part of the request. func (c *githubConnector) userEmail(ctx context.Context, client *http.Client) (string, error) { + var ( + primaryEmail userEmail + preferredEmails []userEmail + ) + apiURL := c.apiURL + "/user/emails" + for { // https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user var ( @@ -605,7 +595,17 @@ func (c *githubConnector) userEmail(ctx context.Context, client *http.Client) (s } if email.Verified && email.Primary { - return email.Email, nil + primaryEmail = email + } + + if c.preferredEmailDomain != "" { + _, domainPart, ok := strings.Cut(email.Email, "@") + if !ok { + return "", errors.New("github: invalid format email is detected") + } + if email.Verified && c.isPreferredEmailDomain(domainPart) { + preferredEmails = append(preferredEmails, email) + } } } @@ -614,7 +614,36 @@ func (c *githubConnector) userEmail(ctx context.Context, client *http.Client) (s } } - return "", errors.New("github: user has no verified, primary email") + if len(preferredEmails) > 0 { + return preferredEmails[0].Email, nil + } + + if primaryEmail.Email != "" { + return primaryEmail.Email, nil + } + + return "", errors.New("github: user has no verified, primary email or preferred-domain email") +} + +// isPreferredEmailDomain checks the domain is matching with preferredEmailDomain. +func (c *githubConnector) isPreferredEmailDomain(domain string) bool { + if domain == c.preferredEmailDomain { + return true + } + + preferredDomainParts := strings.Split(c.preferredEmailDomain, ".") + domainParts := strings.Split(domain, ".") + + if len(preferredDomainParts) != len(domainParts) { + return false + } + + for i, v := range preferredDomainParts { + if domainParts[i] != v && v != "*" { + return false + } + } + return true } // userInOrg queries the GitHub API for a users' org membership. @@ -633,6 +662,7 @@ func (c *githubConnector) userInOrg(ctx context.Context, client *http.Client, us return false, fmt.Errorf("github: new req: %v", err) } req = req.WithContext(ctx) + req.Header.Set("X-GitHub-Api-Version", githubAPIVersion) resp, err := client.Do(req) if err != nil { return false, fmt.Errorf("github: get teams: %v", err) @@ -642,7 +672,7 @@ func (c *githubConnector) userInOrg(ctx context.Context, client *http.Client, us switch resp.StatusCode { case http.StatusNoContent: case http.StatusFound, http.StatusNotFound: - c.logger.Infof("github: user %q not in org %q or application not authorized to read org data", userName, orgName) + c.logger.Info("user not in org or application not authorized to read org data", "user", userName, "org", orgName) default: err = fmt.Errorf("github: unexpected return status: %q", resp.Status) } diff --git a/connector/github/github_test.go b/connector/github/github_test.go index 76d7463cf6..de35149608 100644 --- a/connector/github/github_test.go +++ b/connector/github/github_test.go @@ -4,7 +4,9 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" + "log/slog" "net/http" "net/http/httptest" "net/url" @@ -150,7 +152,7 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) { expectNil(t, err) c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: newClient()} - identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some-login") @@ -158,7 +160,7 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) { expectEquals(t, 0, len(identity.Groups)) c = githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: newClient(), loadAllGroups: true} - identity, err = c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err = c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some-login") @@ -191,13 +193,313 @@ func TestLoginUsedAsIDWhenConfigured(t *testing.T) { expectNil(t, err) c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: newClient(), useLoginAsID: true} - identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.UserID, "some-login") expectEquals(t, identity.Username, "Joe Bloggs") } +func TestPreferredEmailDomainConfigured(t *testing.T) { + ctx := context.Background() + s := newTestServer(map[string]testResponse{ + "/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}}, + "/user/emails": { + data: []userEmail{ + { + Email: "some@email.com", + Verified: true, + Primary: true, + }, + { + Email: "another@email.com", + Verified: true, + Primary: false, + }, + { + Email: "some@preferred-domain.com", + Verified: true, + Primary: false, + }, + { + Email: "another@preferred-domain.com", + Verified: true, + Primary: false, + }, + }, + }, + }) + defer s.Close() + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + client := newClient() + c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "preferred-domain.com"} + + u, err := c.user(ctx, client) + expectNil(t, err) + expectEquals(t, u.Email, "some@preferred-domain.com") +} + +func TestPreferredEmailDomainConfiguredWithGlob(t *testing.T) { + ctx := context.Background() + s := newTestServer(map[string]testResponse{ + "/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}}, + "/user/emails": { + data: []userEmail{ + { + Email: "some@email.com", + Verified: true, + Primary: true, + }, + { + Email: "another@email.com", + Verified: true, + Primary: false, + }, + { + Email: "some@another.preferred-domain.com", + Verified: true, + Primary: false, + }, + { + Email: "some@sub-domain.preferred-domain.co", + Verified: true, + Primary: false, + }, + }, + }, + }) + defer s.Close() + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + client := newClient() + c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "*.preferred-domain.co"} + + u, err := c.user(ctx, client) + expectNil(t, err) + expectEquals(t, u.Email, "some@sub-domain.preferred-domain.co") +} + +func TestPreferredEmailDomainConfigured_UserHasNoPreferredDomainEmail(t *testing.T) { + ctx := context.Background() + s := newTestServer(map[string]testResponse{ + "/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}}, + "/user/emails": { + data: []userEmail{ + { + Email: "some@email.com", + Verified: true, + Primary: true, + }, + { + Email: "another@email.com", + Verified: true, + Primary: false, + }, + }, + }, + }) + defer s.Close() + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + client := newClient() + c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "preferred-domain.com"} + + u, err := c.user(ctx, client) + expectNil(t, err) + expectEquals(t, u.Email, "some@email.com") +} + +func TestPreferredEmailDomainNotConfigured(t *testing.T) { + ctx := context.Background() + s := newTestServer(map[string]testResponse{ + "/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}}, + "/user/emails": { + data: []userEmail{ + { + Email: "some@email.com", + Verified: true, + Primary: true, + }, + { + Email: "another@email.com", + Verified: true, + Primary: false, + }, + { + Email: "some@preferred-domain.com", + Verified: true, + Primary: false, + }, + }, + }, + }) + defer s.Close() + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + client := newClient() + c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client} + + u, err := c.user(ctx, client) + expectNil(t, err) + expectEquals(t, u.Email, "some@email.com") +} + +func TestPreferredEmailDomainConfigured_Error_BothPrimaryAndPreferredDomainEmailNotFound(t *testing.T) { + ctx := context.Background() + s := newTestServer(map[string]testResponse{ + "/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}}, + "/user/emails": { + data: []userEmail{ + { + Email: "some@email.com", + Verified: true, + Primary: false, + }, + { + Email: "another@email.com", + Verified: true, + Primary: false, + }, + { + Email: "some@preferred-domain.com", + Verified: true, + Primary: false, + }, + }, + }, + }) + defer s.Close() + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + client := newClient() + c := githubConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: client, preferredEmailDomain: "foo.bar"} + + _, err = c.user(ctx, client) + expectNotNil(t, err, "Email not found error") + expectEquals(t, err.Error(), "github: user has no verified, primary email or preferred-domain email") +} + +func Test_isPreferredEmailDomain(t *testing.T) { + client := newClient() + tests := []struct { + preferredEmailDomain string + email string + expected bool + }{ + { + preferredEmailDomain: "example.com", + email: "test@example.com", + expected: true, + }, + { + preferredEmailDomain: "example.com", + email: "test@another.com", + expected: false, + }, + { + preferredEmailDomain: "*.example.com", + email: "test@my.example.com", + expected: true, + }, + { + preferredEmailDomain: "*.example.com", + email: "test@my.another.com", + expected: false, + }, + { + preferredEmailDomain: "*.example.com", + email: "test@my.domain.example.com", + expected: false, + }, + { + preferredEmailDomain: "*.example.com", + email: "test@sub.domain.com", + expected: false, + }, + { + preferredEmailDomain: "*.*.example.com", + email: "test@sub.my.example.com", + expected: true, + }, + { + preferredEmailDomain: "*.*.example.com", + email: "test@a.my.google.com", + expected: false, + }, + } + for _, test := range tests { + t.Run(test.preferredEmailDomain, func(t *testing.T) { + c := githubConnector{apiURL: "apiURL", hostName: "github.com", httpClient: client, preferredEmailDomain: test.preferredEmailDomain} + _, domainPart, _ := strings.Cut(test.email, "@") + res := c.isPreferredEmailDomain(domainPart) + + expectEquals(t, res, test.expected) + }) + } +} + +func Test_Open_PreferredDomainConfig(t *testing.T) { + log := slog.New(slog.DiscardHandler) + tests := []struct { + preferredEmailDomain string + email string + expected error + }{ + { + preferredEmailDomain: "example.com", + expected: nil, + }, + { + preferredEmailDomain: "*.example.com", + expected: nil, + }, + { + preferredEmailDomain: "*.*.example.com", + expected: nil, + }, + { + preferredEmailDomain: "example.*", + expected: errors.New("invalid PreferredEmailDomain: glob pattern cannot end with \"*\""), + }, + } + for _, test := range tests { + t.Run(test.preferredEmailDomain, func(t *testing.T) { + c := Config{ + PreferredEmailDomain: test.preferredEmailDomain, + } + _, err := c.Open("id", log) + + expectEquals(t, err, test.expected) + }) + } +} + +func TestGetSendsAPIVersionHeader(t *testing.T) { + var gotHeader string + s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotHeader = r.Header.Get("X-GitHub-Api-Version") + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode([]org{}) + })) + defer s.Close() + + var result []org + _, err := get(context.Background(), newClient(), s.URL+"/user/orgs", &result) + expectNil(t, err) + expectEquals(t, gotHeader, githubAPIVersion) +} + func newTestServer(responses map[string]testResponse) *httptest.Server { var s *httptest.Server s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -231,6 +533,12 @@ func expectNil(t *testing.T, a interface{}) { } } +func expectNotNil(t *testing.T, a interface{}, msg string) { + if a == nil { + t.Errorf("Expected %+v to not to be nil", msg) + } +} + func expectEquals(t *testing.T, a interface{}, b interface{}) { if !reflect.DeepEqual(a, b) { t.Errorf("Expected %+v to equal %+v", a, b) diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go index f35ac35753..b9fb3bec05 100644 --- a/connector/gitlab/gitlab.go +++ b/connector/gitlab/gitlab.go @@ -1,4 +1,4 @@ -// Package gitlab provides authentication strategies using Gitlab. +// Package gitlab provides authentication strategies using GitLab. package gitlab import ( @@ -7,15 +7,17 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "strconv" + "strings" "time" "golang.org/x/oauth2" "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/httpclient" ) const ( @@ -28,12 +30,14 @@ const ( // Config holds configuration options for gitlab logins. type Config struct { - BaseURL string `json:"baseURL"` - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - Groups []string `json:"groups"` - UseLoginAsID bool `json:"useLoginAsID"` + BaseURL string `json:"baseURL"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + Groups []string `json:"groups"` + UseLoginAsID bool `json:"useLoginAsID"` + GetGroupsPermission bool `json:"getGroupsPermission"` + RootCAData []byte `json:"rootCAData,omitempty"` } type gitlabUser struct { @@ -46,18 +50,33 @@ type gitlabUser struct { } // Open returns a strategy for logging in through GitLab. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { if c.BaseURL == "" { c.BaseURL = "https://gitlab.com" } + var httpClient *http.Client + if len(c.RootCAData) > 0 { + var err error + httpClient, err = httpclient.NewHTTPClient([]string{string(c.RootCAData)}, false) + if err != nil { + // Keep backward-compatible error semantics for invalid PEM input. + if strings.Contains(err.Error(), "not in PEM format") { + return nil, fmt.Errorf("gitlab: invalid rootCAData") + } + return nil, fmt.Errorf("gitlab: failed to create HTTP client: %v", err) + } + httpClient.Timeout = 30 * time.Second + } return &gitlabConnector{ - baseURL: c.BaseURL, - redirectURI: c.RedirectURI, - clientID: c.ClientID, - clientSecret: c.ClientSecret, - logger: logger, - groups: c.Groups, - useLoginAsID: c.UseLoginAsID, + baseURL: c.BaseURL, + redirectURI: c.RedirectURI, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + logger: logger.With(slog.Group("connector", "type", "gitlab", "id", id)), + groups: c.Groups, + useLoginAsID: c.UseLoginAsID, + getGroupsPermission: c.GetGroupsPermission, + httpClient: httpClient, }, nil } @@ -68,8 +87,9 @@ type connectorData struct { } var ( - _ connector.CallbackConnector = (*gitlabConnector)(nil) - _ connector.RefreshConnector = (*gitlabConnector)(nil) + _ connector.CallbackConnector = (*gitlabConnector)(nil) + _ connector.RefreshConnector = (*gitlabConnector)(nil) + _ connector.TokenIdentityConnector = (*gitlabConnector)(nil) ) type gitlabConnector struct { @@ -78,10 +98,13 @@ type gitlabConnector struct { groups []string clientID string clientSecret string - logger log.Logger + logger *slog.Logger httpClient *http.Client // if set to true will use the user's handle rather than their numeric id as the ID useLoginAsID bool + + // if set to true permissions will be added to list of groups + getGroupsPermission bool } func (c *gitlabConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config { @@ -100,11 +123,11 @@ func (c *gitlabConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config { } } -func (c *gitlabConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *gitlabConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", c.redirectURI, callbackURL) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", c.redirectURI, callbackURL) } - return c.oauth2Config(scopes).AuthCodeURL(state), nil + return c.oauth2Config(scopes).AuthCodeURL(state), nil, nil } type oauth2Error struct { @@ -119,7 +142,7 @@ func (e *oauth2Error) Error() string { return e.error + ": " + e.errorDescription } -func (c *gitlabConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *gitlabConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} @@ -221,6 +244,34 @@ func (c *gitlabConnector) Refresh(ctx context.Context, s connector.Scopes, ident } } +// TokenIdentity is used for token exchange, verifying a GitLab access token +// and returning the associated user identity. This enables direct authentication +// with Dex using an existing GitLab token without going through the OAuth flow. +// +// Note: The connector decides whether to fetch groups based on its configuration +// (groups filter, getGroupsPermission), not on the scopes from the token exchange request. +// The server will then decide whether to include groups in the final token based on +// the requested scopes. This matches the behavior of other connectors (e.g., OIDC). +func (c *gitlabConnector) TokenIdentity(ctx context.Context, _, subjectToken string) (connector.Identity, error) { + if c.httpClient != nil { + ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient) + } + + token := &oauth2.Token{ + AccessToken: subjectToken, + TokenType: "Bearer", // GitLab tokens are typically Bearer tokens even if the type is not explicitly provided. + } + + // For token exchange, we determine if groups should be fetched based on connector configuration. + // If the connector has groups filter or getGroupsPermission enabled, we fetch groups. + scopes := connector.Scopes{ + // Scopes are not provided in token exchange, so we request groups every time and return only if configured. + Groups: true, + } + + return c.identity(ctx, scopes, token) +} + func (c *gitlabConnector) groupsRequired(groupScope bool) bool { return len(c.groups) > 0 || groupScope } @@ -256,7 +307,10 @@ func (c *gitlabConnector) user(ctx context.Context, client *http.Client) (gitlab } type userInfo struct { - Groups []string + Groups []string `json:"groups"` + OwnerPermission []string `json:"https://gitlab.org/claims/groups/owner"` + MaintainerPermission []string `json:"https://gitlab.org/claims/groups/maintainer"` + DeveloperPermission []string `json:"https://gitlab.org/claims/groups/developer"` } // userGroups queries the GitLab API for group membership. @@ -287,9 +341,62 @@ func (c *gitlabConnector) userGroups(ctx context.Context, client *http.Client) ( return nil, fmt.Errorf("failed to decode response: %v", err) } + if c.getGroupsPermission { + groups := c.setGroupsPermission(u) + return groups, nil + } + return u.Groups, nil } +func (c *gitlabConnector) setGroupsPermission(u userInfo) []string { + groups := u.Groups + +L1: + for _, g := range groups { + for _, op := range u.OwnerPermission { + if g == op { + groups = append(groups, fmt.Sprintf("%s:owner", g)) + continue L1 + } + if len(g) > len(op) { + if g[0:len(op)] == op && string(g[len(op)]) == "/" { + groups = append(groups, fmt.Sprintf("%s:owner", g)) + continue L1 + } + } + } + + for _, mp := range u.MaintainerPermission { + if g == mp { + groups = append(groups, fmt.Sprintf("%s:maintainer", g)) + continue L1 + } + if len(g) > len(mp) { + if g[0:len(mp)] == mp && string(g[len(mp)]) == "/" { + groups = append(groups, fmt.Sprintf("%s:maintainer", g)) + continue L1 + } + } + } + + for _, dp := range u.DeveloperPermission { + if g == dp { + groups = append(groups, fmt.Sprintf("%s:developer", g)) + continue L1 + } + if len(g) > len(dp) { + if g[0:len(dp)] == dp && string(g[len(dp)]) == "/" { + groups = append(groups, fmt.Sprintf("%s:developer", g)) + continue L1 + } + } + } + } + + return groups +} + func (c *gitlabConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string) ([]string, error) { gitlabGroups, err := c.userGroups(ctx, client) if err != nil { diff --git a/connector/gitlab/gitlab_test.go b/connector/gitlab/gitlab_test.go index d828b8bd16..9261464329 100644 --- a/connector/gitlab/gitlab_test.go +++ b/connector/gitlab/gitlab_test.go @@ -4,15 +4,178 @@ import ( "context" "crypto/tls" "encoding/json" + "fmt" + "io" + "log/slog" "net/http" "net/http/httptest" "net/url" + "os" "reflect" + "strings" "testing" + "time" "github.com/dexidp/dex/connector" ) +func readValidRootCAData(t *testing.T) []byte { + t.Helper() + b, err := os.ReadFile("testdata/rootCA.pem") + if err != nil { + t.Fatalf("failed to read rootCA.pem testdata: %v", err) + } + return b +} + +func newLocalHTTPSTestServer(t *testing.T, handler http.Handler) *httptest.Server { + t.Helper() + + ts := httptest.NewUnstartedServer(handler) + cert, err := tls.LoadX509KeyPair("testdata/server.crt", "testdata/server.key") + if err != nil { + t.Fatalf("failed to load TLS test cert/key: %v", err) + } + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} + ts.StartTLS() + return ts +} + +func TestOpenWithRootCADataCreatesHTTPClient(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + cfg := &Config{ + RootCAData: readValidRootCAData(t), + } + + conn, err := cfg.Open("test", logger) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + gc, ok := conn.(*gitlabConnector) + if !ok { + t.Fatalf("expected *gitlabConnector, got %T", conn) + } + if gc.httpClient == nil { + t.Fatalf("expected httpClient to be non-nil") + } + if gc.httpClient.Timeout != 30*time.Second { + t.Fatalf("expected httpClient timeout %v, got %v", 30*time.Second, gc.httpClient.Timeout) + } + tr, ok := gc.httpClient.Transport.(*http.Transport) + if !ok { + t.Fatalf("expected transport to be *http.Transport, got %T", gc.httpClient.Transport) + } + // ProxyFromEnvironment is expected to be enabled (non-nil proxy func). + if tr.Proxy == nil { + t.Fatalf("expected transport.Proxy to be set (ProxyFromEnvironment)") + } + if tr.TLSClientConfig == nil || tr.TLSClientConfig.RootCAs == nil { + t.Fatalf("expected transport TLS root CAs to be configured") + } +} + +func TestOpenWithInvalidRootCADataReturnsError(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + cfg := &Config{ + RootCAData: []byte("not a pem"), + } + + _, err := cfg.Open("test", logger) + if err == nil { + t.Fatalf("expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid rootCAData") { + t.Fatalf("expected error to contain %q, got %q", "invalid rootCAData", err.Error()) + } +} + +func TestHandleCallbackCustomRootCADataEnablesTLSRequests(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + ts := newLocalHTTPSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + switch r.URL.Path { + case "/oauth/token": + // oauth2.Exchange expects an access token in response. + fmt.Fprint(w, `{"access_token":"abc","token_type":"bearer","expires_in":30}`) + case "/api/v4/user": + json.NewEncoder(w).Encode(gitlabUser{Email: "some@email.com", ID: 12345678}) + default: + http.NotFound(w, r) + } + })) + defer ts.Close() + + cfg := &Config{ + BaseURL: ts.URL, + ClientID: "client-id", + ClientSecret: "client-secret", + RedirectURI: "https://example.invalid/callback", + RootCAData: readValidRootCAData(t), + } + + conn, err := cfg.Open("test", logger) + if err != nil { + t.Fatalf("Open() error: %v", err) + } + + hostURL, err := url.Parse(ts.URL) + expectNil(t, err) + req, err := http.NewRequest("GET", hostURL.String()+"?code=testcode", nil) + expectNil(t, err) + + identity, err := conn.(connector.CallbackConnector).HandleCallback(connector.Scopes{Groups: false}, nil, req) + if err != nil { + t.Fatalf("HandleCallback() error: %v", err) + } + if identity.Email != "some@email.com" || identity.UserID != "12345678" { + t.Fatalf("unexpected identity: %#v", identity) + } +} + +func TestHandleCallbackWithoutRootCADataFailsTLS(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + ts := newLocalHTTPSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + switch r.URL.Path { + case "/oauth/token": + fmt.Fprint(w, `{"access_token":"abc","token_type":"bearer","expires_in":30}`) + case "/api/v4/user": + json.NewEncoder(w).Encode(gitlabUser{Email: "some@email.com", ID: 12345678}) + default: + http.NotFound(w, r) + } + })) + defer ts.Close() + + cfg := &Config{ + BaseURL: ts.URL, + ClientID: "client-id", + ClientSecret: "client-secret", + RedirectURI: "https://example.invalid/callback", + // RootCAData intentionally omitted: should fail TLS verification against our custom server cert. + } + + conn, err := cfg.Open("test", logger) + if err != nil { + t.Fatalf("Open() error: %v", err) + } + + hostURL, err := url.Parse(ts.URL) + expectNil(t, err) + req, err := http.NewRequest("GET", hostURL.String()+"?code=testcode", nil) + expectNil(t, err) + + _, err = conn.(connector.CallbackConnector).HandleCallback(connector.Scopes{Groups: false}, nil, req) + if err == nil { + t.Fatalf("expected TLS error, got nil") + } +} + func TestUserGroups(t *testing.T) { s := newTestServer(map[string]interface{}{ "/oauth/userinfo": userInfo{ @@ -84,7 +247,7 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) { expectNil(t, err) c := gitlabConnector{baseURL: s.URL, httpClient: newClient()} - identity, err := c.HandleCallback(connector.Scopes{Groups: false}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: false}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some@email.com") @@ -92,7 +255,7 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) { expectEquals(t, 0, len(identity.Groups)) c = gitlabConnector{baseURL: s.URL, httpClient: newClient()} - identity, err = c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err = c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some@email.com") @@ -120,7 +283,7 @@ func TestLoginUsedAsIDWhenConfigured(t *testing.T) { expectNil(t, err) c := gitlabConnector{baseURL: s.URL, httpClient: newClient(), useLoginAsID: true} - identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.UserID, "joebloggs") @@ -147,7 +310,7 @@ func TestLoginWithTeamWhitelisted(t *testing.T) { expectNil(t, err) c := gitlabConnector{baseURL: s.URL, httpClient: newClient(), groups: []string{"team-1"}} - identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.UserID, "12345678") @@ -174,7 +337,7 @@ func TestLoginWithTeamNonWhitelisted(t *testing.T) { expectNil(t, err) c := gitlabConnector{baseURL: s.URL, httpClient: newClient(), groups: []string{"team-2"}} - _, err = c.HandleCallback(connector.Scopes{Groups: true}, req) + _, err = c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNotNil(t, err, "HandleCallback error") expectEquals(t, err.Error(), "gitlab: get groups: gitlab: user \"joebloggs\" is not in any of the required groups") @@ -208,7 +371,7 @@ func TestRefresh(t *testing.T) { }) expectNil(t, err) - identity, err := c.HandleCallback(connector.Scopes{OfflineAccess: true}, req) + identity, err := c.HandleCallback(connector.Scopes{OfflineAccess: true}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "some@email.com") expectEquals(t, identity.UserID, "12345678") @@ -249,6 +412,47 @@ func TestRefreshWithEmptyConnectorData(t *testing.T) { expectEquals(t, emptyIdentity, identity) } +func TestGroupsWithPermission(t *testing.T) { + s := newTestServer(map[string]interface{}{ + "/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"}, + "/oauth/token": map[string]interface{}{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", + "expires_in": "30", + }, + "/oauth/userinfo": userInfo{ + Groups: []string{"ops", "dev", "ops-test", "ops/project", "dev/project1", "dev/project2"}, + OwnerPermission: []string{"ops"}, + DeveloperPermission: []string{"dev"}, + MaintainerPermission: []string{"dev/project1"}, + }, + }) + defer s.Close() + + hostURL, err := url.Parse(s.URL) + expectNil(t, err) + + req, err := http.NewRequest("GET", hostURL.String(), nil) + expectNil(t, err) + + c := gitlabConnector{baseURL: s.URL, httpClient: newClient(), getGroupsPermission: true} + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) + expectNil(t, err) + + expectEquals(t, identity.Groups, []string{ + "ops", + "dev", + "ops-test", + "ops/project", + "dev/project1", + "dev/project2", + "ops:owner", + "dev:developer", + "ops/project:owner", + "dev/project1:maintainer", + "dev/project2:developer", + }) +} + func newTestServer(responses map[string]interface{}) *httptest.Server { return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { response := responses[r.RequestURI] @@ -281,3 +485,88 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) { t.Errorf("Expected %+v to equal %+v", a, b) } } + +func TestTokenIdentity(t *testing.T) { + // Note: These tests verify that the connector returns groups based on its configuration. + // The actual inclusion of groups in the final Dex token depends on the 'groups' scope + // in the token exchange request, which is handled by the Dex server, not the connector. + tests := []struct { + name string + userInfo userInfo + groups []string + getGroupsPermission bool + useLoginAsID bool + expectUserID string + expectGroups []string + }{ + { + name: "without groups config", + expectUserID: "12345678", + expectGroups: nil, + }, + { + name: "with groups filter", + userInfo: userInfo{ + Groups: []string{"team-1", "team-2"}, + }, + groups: []string{"team-1"}, + expectUserID: "12345678", + expectGroups: []string{"team-1"}, + }, + { + name: "with groups permission", + userInfo: userInfo{ + Groups: []string{"ops", "dev"}, + OwnerPermission: []string{"ops"}, + DeveloperPermission: []string{"dev"}, + MaintainerPermission: []string{}, + }, + getGroupsPermission: true, + expectUserID: "12345678", + expectGroups: []string{"ops", "dev", "ops:owner", "dev:developer"}, + }, + { + name: "with useLoginAsID", + useLoginAsID: true, + expectUserID: "joebloggs", + expectGroups: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + responses := map[string]interface{}{ + "/api/v4/user": gitlabUser{ + Email: "some@email.com", + ID: 12345678, + Name: "Joe Bloggs", + Username: "joebloggs", + }, + "/oauth/userinfo": tc.userInfo, + } + + s := newTestServer(responses) + defer s.Close() + + c := gitlabConnector{ + baseURL: s.URL, + httpClient: newClient(), + groups: tc.groups, + getGroupsPermission: tc.getGroupsPermission, + useLoginAsID: tc.useLoginAsID, + } + + accessToken := "test-access-token" + ctx := context.Background() + identity, err := c.TokenIdentity(ctx, "urn:ietf:params:oauth:token-type:access_token", accessToken) + + expectNil(t, err) + expectEquals(t, identity.UserID, tc.expectUserID) + expectEquals(t, identity.Username, "Joe Bloggs") + expectEquals(t, identity.PreferredUsername, "joebloggs") + expectEquals(t, identity.Email, "some@email.com") + expectEquals(t, identity.EmailVerified, true) + expectEquals(t, identity.Groups, tc.expectGroups) + }) + } +} diff --git a/connector/gitlab/testdata/rootCA.pem b/connector/gitlab/testdata/rootCA.pem new file mode 100644 index 0000000000..c03bdac0c0 --- /dev/null +++ b/connector/gitlab/testdata/rootCA.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID1jCCAr4CCQCG4JBeSi6cDjANBgkqhkiG9w0BAQsFADCBrDELMAkGA1UEBhMC +VVMxFDASBgNVBAgMC1JhbmRvbVN0YXRlMRMwEQYDVQQHDApSYW5kb21DaXR5MRsw +GQYDVQQKDBJSYW5kb21Pcmdhbml6YXRpb24xHzAdBgNVBAsMFlJhbmRvbU9yZ2Fu +aXphdGlvblVuaXQxIDAeBgkqhkiG9w0BCQEWEWhlbGxvQGV4YW1wbGUuY29tMRIw +EAYDVQQDDAlsb2NhbGhvc3QwHhcNMjIxMDA3MjIwNjQwWhcNMzIxMDA0MjIwNjQw +WjCBrDELMAkGA1UEBhMCVVMxFDASBgNVBAgMC1JhbmRvbVN0YXRlMRMwEQYDVQQH +DApSYW5kb21DaXR5MRswGQYDVQQKDBJSYW5kb21Pcmdhbml6YXRpb24xHzAdBgNV +BAsMFlJhbmRvbU9yZ2FuaXphdGlvblVuaXQxIDAeBgkqhkiG9w0BCQEWEWhlbGxv +QGV4YW1wbGUuY29tMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDh0HlpAKMKYyxbvW70XRY2bVNiNdAFninug1P4FDAJ +z8xnbFzk17FLY7zqdtGTDmPDJ8AAxIwpGv2zYWW5VMeqKWfvyuD5dSCauY1Pdmug +uZbpAvoJrx1sw+TL61ByVmy8x3ccB4LLKuzil/vAzUDJQkPsfTECVUPV+yiGSDuO +EEVR9X6rZUwx2expXm8Wtb/a88FbPVI09b9eb4iWfLvGD2eNAtw8w21W0X7sQ8Hq +zEPqquMEL4qPnNDdtk592uHvLLrd1uH8qH7c1JyA76T7H3YeUCNEi+PnLgqtsZmX +sKY62HnLt8/LAClVsN9lFYkKEjU9V+U7IN2cL6+EwtsdAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAN6g0qit/3R2X+KdR0LgRXF/h4qQFgcV6cxnhRAmLIDNJlxKSHqN +IE5+bxzCbkblzGfr/jNPqW0s+yaN4CyMgKNYSzkLBPE4FF+19Uv+dyYfFms3mDJ7 +0rGjS5bCscThWhpaSw20LcwQcr/+X+/fGzJ01dVFK1UOjBKg4d4dMwxklbIkZqIq +siRW0GMy26mgVZ/BSjeh5kEjs6h6H3cJsGl7xYT+BI7wnxHwGeT9tkBgiyT5FwaS +vtdZkBpQ9q8f7FwsEm3woLHdWuOnrtUtVpY/oc6WFGdROQdGzjSk0D3kHs9YhueC +GSzZKrqX+TSIgpPrLYNHX4uxlo5TAwP/5GM= +-----END CERTIFICATE----- diff --git a/connector/gitlab/testdata/server.crt b/connector/gitlab/testdata/server.crt new file mode 100644 index 0000000000..9b0f12ec58 --- /dev/null +++ b/connector/gitlab/testdata/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5TCCA82gAwIBAgIJAMGzXwBRpkG7MA0GCSqGSIb3DQEBCwUAMIGsMQswCQYD +VQQGEwJVUzEUMBIGA1UECAwLUmFuZG9tU3RhdGUxEzARBgNVBAcMClJhbmRvbUNp +dHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXphdGlvbjEfMB0GA1UECwwWUmFuZG9t +T3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3DQEJARYRaGVsbG9AZXhhbXBsZS5j +b20xEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMjEwMDcyMjA3MDhaFw0zMjEwMDQy +MjA3MDhaMIGsMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLUmFuZG9tU3RhdGUxEzAR +BgNVBAcMClJhbmRvbUNpdHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXphdGlvbjEf +MB0GA1UECwwWUmFuZG9tT3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3DQEJARYR +aGVsbG9AZXhhbXBsZS5jb20xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMuKdpXP87Q7Kg3iafXzvBuVIyV1K5UmMYiN +koztkC5XrCzHaQRS/CoIb7/nUqmtAxx7RL0jzhZ93zBN4HY/Zcnrd9tXoPPxi0mG +ZZWfFU6nN8nOkMHWzEbHVBmhxpfGtwmLcajQ4HrK1TZwJUn6GqclHQRy/gjxkiw5 +KPqzfVOVlA6ht4KdKstKazQkWZ5gdWT4d8yrEy/IT4oaW05xALBMQ7YGjkzWKsSF +6ygXI7xqF9rg9jCnUsPYg4f8ut3N0c00KjsfKOOj2dF/ZyjedQ5c0u4hHmxSo3Ka +0ZTmIrMfbVXgGjxRG2HZXLpPvQKoCf/fOX8Irdr+lahFVKASxN0CAwEAAaOCAQYw +ggECMIHLBgNVHSMEgcMwgcChgbKkga8wgawxCzAJBgNVBAYTAlVTMRQwEgYDVQQI +DAtSYW5kb21TdGF0ZTETMBEGA1UEBwwKUmFuZG9tQ2l0eTEbMBkGA1UECgwSUmFu +ZG9tT3JnYW5pemF0aW9uMR8wHQYDVQQLDBZSYW5kb21Pcmdhbml6YXRpb25Vbml0 +MSAwHgYJKoZIhvcNAQkBFhFoZWxsb0BleGFtcGxlLmNvbTESMBAGA1UEAwwJbG9j +YWxob3N0ggkAhuCQXkounA4wCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwGgYDVR0R +BBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCWmh5ebpkm +v2B1yQgarSCSSkLZ5DZSAJjrPgW2IJqCW2q2D1HworbW1Yn5jqrM9FKGnJfjCyve +zBB5AOlGp+0bsZGgMRMCavgv4QhTThXUoJqqHcfEu4wHndcgrqSadxmV5aisSR4u +gXnjW43o3akby+h1K40RR3vVkpzPaoC3/bgk7WVpfpPiP32E24a01gETozRb/of/ +ATN3JBe0xh+e63CrPX1sago5+u3UETIoOr0fW8M/gU9GApmJiFAXwHag6j54hLCG +23EtVDwmlarG8Pj+i0yru8s22QqzAJi5E0OwR4aB8tqicLKYBVfzyLCOielIBUrK +OkuFKp+VjxQX +-----END CERTIFICATE----- diff --git a/connector/gitlab/testdata/server.key b/connector/gitlab/testdata/server.key new file mode 100644 index 0000000000..9708e1e6ea --- /dev/null +++ b/connector/gitlab/testdata/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLinaVz/O0OyoN +4mn187wblSMldSuVJjGIjZKM7ZAuV6wsx2kEUvwqCG+/51KprQMce0S9I84Wfd8w +TeB2P2XJ63fbV6Dz8YtJhmWVnxVOpzfJzpDB1sxGx1QZocaXxrcJi3Go0OB6ytU2 +cCVJ+hqnJR0Ecv4I8ZIsOSj6s31TlZQOobeCnSrLSms0JFmeYHVk+HfMqxMvyE+K +GltOcQCwTEO2Bo5M1irEhesoFyO8ahfa4PYwp1LD2IOH/LrdzdHNNCo7Hyjjo9nR +f2co3nUOXNLuIR5sUqNymtGU5iKzH21V4Bo8URth2Vy6T70CqAn/3zl/CK3a/pWo +RVSgEsTdAgMBAAECggEAU6cxu7q+54kVbKVsdThaTF/MFR4F7oPHAd9lpuQQSOuh +iLngMHXGy6OyAgYZlEDWMYN8KdwoXFgZPaoUIaVGuWk8Vnq6XOgeHfbNk2PRhwT0 +yc1K80/Lnx9XMj2p+EEkgxi7eu12BSGN5ZTLzo6rG50GQwjb3WMjd2d6rybL0GjC +wg2arcBk3sSMYmvZOqlAsaQmtgwkJhvhVkVfEQSD3VKF7g0dh/h3LIPyM0Ff4M67 +KpLMPPwzUJ/0Z4ewAP06mMKUA86R93M+dWs2eh1oBGnRkVQdhCJLXJpuGHZ6BTiB +Ry0AeorHfnVXPbtpUeAq6m5/BBl6qX0ooB08BIFwAQKBgQDqJpTZS/ZzqL6Kcs14 +MyFu+7DungSxQ5oK9ju7EFSosanSk4UEa/lw992kM6nsIMwgSVQgba5zKcVMeSmk +AVbpznegQD1BYCwOGwbGvkJ8jbhPy+WLbbRjWT/E6AItZgUK+fyTIcNvSehcQqsT +fhgWsK7ueZCmLQfVhK1AxtvY3QKBgQDeiKuo8plsH/7IxDn7KVHBOHKPC2ZPzg03 +i7La6zomiRckwwPnhicRSYsjtfCCW6Ms+uzjTEItgFM+5PdrXheeku+z/sExRtZu +emqPqDomixlXDRQ6RN3gnBSk4RU+ROB1u1uBLWXqRz8Gp2zJGRxhHfYt2zefBv4w +/cIuPC3cAQKBgD2UsAkGJWb9tj8LOmama+CYaUwYWvuT3+uKHuNvxBQpxZQQICet +jgjb53rL66Cib4z+PBXbQsoe7jjSlNUBVS5gkq2et31+IZgEG6AhYbMIQrUZ1uD4 +lTybuF289vWhoynj3T2E37VhJq89CWky/HrbNOabKiPKLAlHv5kNs7wxAoGBANEJ +XQbU7J2O6Iy7FyQBSlTQq3wHX1Iz4mJ9DcNrFzK/sEfOEMrZT7WDefpPm984KW3F +P+S766ZGVuxLtMbcmh9RM23HLr8VJbSdtZ/AjO9L1r/Y/1lE+49TzmibLpNRq++r +0WbkuEl8J44ek6fLuMbZmDi3JeZycTCgDlnUGdgBAoGAYdliovtURZCm46t1uE3F +idCLCXCccjkt1hcNGNjck/b0trHA7wOEqICIguoWDlEBTc0PDvHEq6PfKyqptGkj +AgaZTMF/aZiGqlT7VRpBuzxM/uV5xzCg+i2ViaW/p3xq0z2PRljVZiEfe5aWcjiM +ouTtnC3TgmcjhTgGmb48QQE= +-----END PRIVATE KEY----- diff --git a/connector/google/google.go b/connector/google/google.go index 72cc6a18a5..4a8599c0b1 100644 --- a/connector/google/google.go +++ b/connector/google/google.go @@ -5,23 +5,28 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "os" + "strings" "time" + "cloud.google.com/go/compute/metadata" "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/exp/slices" "golang.org/x/oauth2" "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/impersonate" "google.golang.org/api/option" "github.com/dexidp/dex/connector" pkg_groups "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" ) const ( - issuerURL = "https://accounts.google.com" + issuerURL = "https://accounts.google.com" + wildcardDomainToAdminEmail = "*" ) // Config holds configuration options for Google logins. @@ -45,17 +50,33 @@ type Config struct { // check groups with the admin directory api ServiceAccountFilePath string `json:"serviceAccountFilePath"` + // Deprecated: Use DomainToAdminEmail + AdminEmail string + // Required if ServiceAccountFilePath - // The email of a GSuite super user which the service account will impersonate + // The map workspace domain to email of a GSuite super user which the service account will impersonate // when listing groups - AdminEmail string + DomainToAdminEmail map[string]string // If this field is true, fetch direct group membership and transitive group membership FetchTransitiveGroupMembership bool `json:"fetchTransitiveGroupMembership"` + + // Optional value for the prompt parameter, defaults to consent when offline_access + // scope is requested + PromptType *string `json:"promptType"` } // Open returns a connector which can be used to login users through Google. -func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) { +func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) { + logger = logger.With(slog.Group("connector", "type", "google", "id", id)) + if c.AdminEmail != "" { + logger.Warn(`use "domainToAdminEmail.*" option instead of "adminEmail"`, "deprecated", true) + if c.DomainToAdminEmail == nil { + c.DomainToAdminEmail = make(map[string]string) + } + + c.DomainToAdminEmail[wildcardDomainToAdminEmail] = c.AdminEmail + } ctx, cancel := context.WithCancel(context.Background()) provider, err := oidc.NewProvider(ctx, issuerURL) @@ -71,10 +92,30 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e scopes = append(scopes, "profile", "email") } - srv, err := createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail, logger) - if err != nil { + adminSrv := make(map[string]*admin.Service) + + // We know impersonation is required when using a service account credential + // TODO: or is it? + if len(c.DomainToAdminEmail) == 0 && c.ServiceAccountFilePath != "" { cancel() - return nil, fmt.Errorf("could not create directory service: %v", err) + return nil, fmt.Errorf("directory service requires the domainToAdminEmail option to be configured") + } + + if (len(c.DomainToAdminEmail) > 0) || slices.Contains(scopes, "groups") { + for domain, adminEmail := range c.DomainToAdminEmail { + srv, err := createDirectoryService(c.ServiceAccountFilePath, adminEmail, logger) + if err != nil { + cancel() + return nil, fmt.Errorf("could not create directory service: %v", err) + } + + adminSrv[domain] = srv + } + } + + promptType := "consent" + if c.PromptType != nil { + promptType = *c.PromptType } clientID := c.ClientID @@ -95,9 +136,10 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e hostedDomains: c.HostedDomains, groups: c.Groups, serviceAccountFilePath: c.ServiceAccountFilePath, - adminEmail: c.AdminEmail, + domainToAdminEmail: c.DomainToAdminEmail, fetchTransitiveGroupMembership: c.FetchTransitiveGroupMembership, - adminSrv: srv, + adminSrv: adminSrv, + promptType: promptType, }, nil } @@ -111,13 +153,14 @@ type googleConnector struct { oauth2Config *oauth2.Config verifier *oidc.IDTokenVerifier cancel context.CancelFunc - logger log.Logger + logger *slog.Logger hostedDomains []string groups []string serviceAccountFilePath string - adminEmail string + domainToAdminEmail map[string]string fetchTransitiveGroupMembership bool - adminSrv *admin.Service + adminSrv map[string]*admin.Service + promptType string } func (c *googleConnector) Close() error { @@ -125,9 +168,9 @@ func (c *googleConnector) Close() error { return nil } -func (c *googleConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { +func (c *googleConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } var opts []oauth2.AuthCodeOption @@ -140,9 +183,10 @@ func (c *googleConnector) LoginURL(s connector.Scopes, callbackURL, state string } if s.OfflineAccess { - opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")) + opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", c.promptType)) } - return c.oauth2Config.AuthCodeURL(state, opts...), nil + + return c.oauth2Config.AuthCodeURL(state, opts...), nil, nil } type oauth2Error struct { @@ -157,7 +201,7 @@ func (e *oauth2Error) Error() string { return e.error + ": " + e.errorDescription } -func (c *googleConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *googleConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} @@ -218,8 +262,9 @@ func (c *googleConnector) createIdentity(ctx context.Context, identity connector } var groups []string - if s.Groups && c.adminSrv != nil { - groups, err = c.getGroups(claims.Email, c.fetchTransitiveGroupMembership) + if s.Groups && len(c.adminSrv) > 0 { + checkedGroups := make(map[string]struct{}) + groups, err = c.getGroups(claims.Email, c.fetchTransitiveGroupMembership, checkedGroups) if err != nil { return identity, fmt.Errorf("google: could not retrieve groups: %v", err) } @@ -245,30 +290,43 @@ func (c *googleConnector) createIdentity(ctx context.Context, identity connector // getGroups creates a connection to the admin directory service and lists // all groups the user is a member of -func (c *googleConnector) getGroups(email string, fetchTransitiveGroupMembership bool) ([]string, error) { +func (c *googleConnector) getGroups(email string, fetchTransitiveGroupMembership bool, checkedGroups map[string]struct{}) ([]string, error) { var userGroups []string var err error groupsList := &admin.Groups{} + domain := c.extractDomainFromEmail(email) + adminSrv, err := c.findAdminService(domain) + if err != nil { + return nil, err + } + for { - groupsList, err = c.adminSrv.Groups.List(). + groupsList, err = adminSrv.Groups.List(). UserKey(email).PageToken(groupsList.NextPageToken).Do() if err != nil { return nil, fmt.Errorf("could not list groups: %v", err) } for _, group := range groupsList.Groups { + if _, exists := checkedGroups[group.Email]; exists { + continue + } + + checkedGroups[group.Email] = struct{}{} // TODO (joelspeed): Make desired group key configurable userGroups = append(userGroups, group.Email) - // getGroups takes a user's email/alias as well as a group's email/alias - if fetchTransitiveGroupMembership { - transitiveGroups, err := c.getGroups(group.Email, fetchTransitiveGroupMembership) - if err != nil { - return nil, fmt.Errorf("could not list transitive groups: %v", err) - } + if !fetchTransitiveGroupMembership { + continue + } - userGroups = append(userGroups, transitiveGroups...) + // getGroups takes a user's email/alias as well as a group's email/alias + transitiveGroups, err := c.getGroups(group.Email, fetchTransitiveGroupMembership, checkedGroups) + if err != nil { + return nil, fmt.Errorf("could not list transitive groups: %v", err) } + + userGroups = append(userGroups, transitiveGroups...) } if groupsList.NextPageToken == "" { @@ -276,51 +334,124 @@ func (c *googleConnector) getGroups(email string, fetchTransitiveGroupMembership } } - return uniqueGroups(userGroups), nil + return userGroups, nil +} + +func (c *googleConnector) findAdminService(domain string) (*admin.Service, error) { + adminSrv, ok := c.adminSrv[domain] + if !ok { + adminSrv, ok = c.adminSrv[wildcardDomainToAdminEmail] + c.logger.Debug("using wildcard admin email to fetch groups", "admin_email", c.domainToAdminEmail[wildcardDomainToAdminEmail]) + } + + if !ok { + return nil, fmt.Errorf("unable to find super admin email, domainToAdminEmail for domain: %s not set, %s is also empty", domain, wildcardDomainToAdminEmail) + } + + return adminSrv, nil +} + +// extracts the domain name from an email input. If the email is valid, it returns the domain name after the "@" symbol. +// However, in the case of a broken or invalid email, it returns a wildcard symbol. +func (c *googleConnector) extractDomainFromEmail(email string) string { + at := strings.LastIndex(email, "@") + if at >= 0 { + _, domain := email[:at], email[at+1:] + + return domain + } + + return wildcardDomainToAdminEmail +} + +// getCredentialsFromFilePath reads and returns the service account credentials from the file at the provided path. +// If an error occurs during the read, it is returned. +func getCredentialsFromFilePath(serviceAccountFilePath string) ([]byte, error) { + jsonCredentials, err := os.ReadFile(serviceAccountFilePath) + if err != nil { + return nil, fmt.Errorf("error reading credentials from file: %v", err) + } + return jsonCredentials, nil +} + +// getCredentialsFromDefault retrieves the application's default credentials. +// If the default credential is empty, it attempts to create a new service with metadata credentials. +// If successful, it returns the service and nil error. +// If unsuccessful, it returns the error and a nil service. +func getCredentialsFromDefault(ctx context.Context, email string, logger *slog.Logger) ([]byte, *admin.Service, error) { + credential, err := google.FindDefaultCredentials(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch application default credentials: %w", err) + } + + if credential.JSON == nil { + logger.Info("JSON is empty, using flow for GCE") + service, err := createServiceWithMetadataServer(ctx, email, logger) + if err != nil { + return nil, nil, err + } + return nil, service, nil + } + + return credential.JSON, nil, nil +} + +// createServiceWithMetadataServer creates a new service using metadata server. +// If an error occurs during the process, it is returned along with a nil service. +func createServiceWithMetadataServer(ctx context.Context, adminEmail string, logger *slog.Logger) (*admin.Service, error) { + serviceAccountEmail, err := metadata.EmailWithContext(ctx, "default") + logger.Info("discovered serviceAccountEmail", "email", serviceAccountEmail) + + if err != nil { + return nil, fmt.Errorf("unable to get service account email from metadata server: %v", err) + } + + config := impersonate.CredentialsConfig{ + TargetPrincipal: serviceAccountEmail, + Scopes: []string{admin.AdminDirectoryGroupReadonlyScope}, + Lifetime: 0, + Subject: adminEmail, + } + + tokenSource, err := impersonate.CredentialsTokenSource(ctx, config) + if err != nil { + return nil, fmt.Errorf("unable to impersonate with %s, error: %v", adminEmail, err) + } + + return admin.NewService(ctx, option.WithHTTPClient(oauth2.NewClient(ctx, tokenSource))) } // createDirectoryService sets up super user impersonation and creates an admin client for calling // the google admin api. If no serviceAccountFilePath is defined, the application default credential // is used. -func createDirectoryService(serviceAccountFilePath, email string, logger log.Logger) (*admin.Service, error) { - if email == "" { - return nil, fmt.Errorf("directory service requires adminEmail") - } - +func createDirectoryService(serviceAccountFilePath, email string, logger *slog.Logger) (service *admin.Service, err error) { var jsonCredentials []byte - var err error ctx := context.Background() if serviceAccountFilePath == "" { logger.Warn("the application default credential is used since the service account file path is not used") - credential, err := google.FindDefaultCredentials(ctx) + jsonCredentials, service, err = getCredentialsFromDefault(ctx, email, logger) if err != nil { - return nil, fmt.Errorf("failed to fetch application default credentials: %w", err) + return + } + if service != nil { + return } - jsonCredentials = credential.JSON } else { - jsonCredentials, err = os.ReadFile(serviceAccountFilePath) + jsonCredentials, err = getCredentialsFromFilePath(serviceAccountFilePath) if err != nil { - return nil, fmt.Errorf("error reading credentials from file: %v", err) + return } } config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope) if err != nil { - return nil, fmt.Errorf("unable to parse credentials to config: %v", err) + return nil, fmt.Errorf("unable to parse client secret file to config: %v", err) } - config.Subject = email - return admin.NewService(ctx, option.WithHTTPClient(config.Client(ctx))) -} -// uniqueGroups returns the unique groups of a slice -func uniqueGroups(groups []string) []string { - keys := make(map[string]struct{}) - unique := []string{} - for _, group := range groups { - if _, exists := keys[group]; !exists { - keys[group] = struct{}{} - unique = append(unique, group) - } + // Only attempt impersonation when there is a user configured + if email != "" { + config.Subject = email } - return unique + + return admin.NewService(ctx, option.WithHTTPClient(config.Client(ctx))) } diff --git a/connector/google/google_test.go b/connector/google/google_test.go index 5cecbec994..ce0e017cf8 100644 --- a/connector/google/google_test.go +++ b/connector/google/google_test.go @@ -1,31 +1,57 @@ package google import ( + "context" "encoding/json" "fmt" + "log/slog" "net/http" "net/http/httptest" + "net/url" "os" + "strings" "testing" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/option" + + "github.com/dexidp/dex/connector" +) + +var ( + // groups_0 + // ┌───────┤ + // groups_2 groups_1 + // │ ├────────┐ + // └── user_1 user_2 + testGroups = map[string][]*admin.Group{ + "user_1@dexidp.com": {{Email: "groups_2@dexidp.com"}, {Email: "groups_1@dexidp.com"}}, + "user_2@dexidp.com": {{Email: "groups_1@dexidp.com"}}, + "groups_1@dexidp.com": {{Email: "groups_0@dexidp.com"}}, + "groups_2@dexidp.com": {{Email: "groups_0@dexidp.com"}}, + "groups_0@dexidp.com": {}, + } + callCounter = make(map[string]int) ) -func testSetup(t *testing.T) *httptest.Server { +func testSetup() *httptest.Server { mux := http.NewServeMux() - // TODO: mock calls - // mux.HandleFunc("/admin/directory/v1/groups", func(w http.ResponseWriter, r *http.Request) { - // w.Header().Add("Content-Type", "application/json") - // json.NewEncoder(w).Encode(&admin.Groups{ - // Groups: []*admin.Group{}, - // }) - // }) + + mux.HandleFunc("/admin/directory/v1/groups/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + userKey := r.URL.Query().Get("userKey") + if groups, ok := testGroups[userKey]; ok { + json.NewEncoder(w).Encode(admin.Groups{Groups: groups}) + callCounter[userKey]++ + } + }) + return httptest.NewServer(mux) } -func newConnector(config *Config, serverURL string) (*googleConnector, error) { - log := logrus.New() +func newConnector(config *Config) (*googleConnector, error) { + log := slog.New(slog.DiscardHandler) conn, err := config.Open("id", log) if err != nil { return nil, err @@ -56,7 +82,7 @@ func tempServiceAccountKey() (string, error) { } func TestOpen(t *testing.T) { - ts := testSetup(t) + ts := testSetup() defer ts.Close() type testCase struct { @@ -64,7 +90,7 @@ func TestOpen(t *testing.T) { expectedErr string // string to set in GOOGLE_APPLICATION_CREDENTIALS. As local development environments can - // already contain ADC, test cases will be built uppon this setting this env variable + // already contain ADC, test cases will be built upon this setting this env variable adc string } @@ -74,12 +100,13 @@ func TestOpen(t *testing.T) { for name, reference := range map[string]testCase{ "missing_admin_email": { config: &Config{ - ClientID: "testClient", - ClientSecret: "testSecret", - RedirectURI: ts.URL + "/callback", - Scopes: []string{"openid", "groups"}, + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups"}, + ServiceAccountFilePath: serviceAccountFilePath, }, - expectedErr: "requires adminEmail", + expectedErr: "requires the domainToAdminEmail", }, "service_account_key_not_found": { config: &Config{ @@ -87,7 +114,7 @@ func TestOpen(t *testing.T) { ClientSecret: "testSecret", RedirectURI: ts.URL + "/callback", Scopes: []string{"openid", "groups"}, - AdminEmail: "foo@bar.com", + DomainToAdminEmail: map[string]string{"*": "foo@bar.com"}, ServiceAccountFilePath: "not_found.json", }, expectedErr: "error reading credentials", @@ -98,18 +125,18 @@ func TestOpen(t *testing.T) { ClientSecret: "testSecret", RedirectURI: ts.URL + "/callback", Scopes: []string{"openid", "groups"}, - AdminEmail: "foo@bar.com", + DomainToAdminEmail: map[string]string{"bar.com": "foo@bar.com"}, ServiceAccountFilePath: serviceAccountFilePath, }, expectedErr: "", }, "adc": { config: &Config{ - ClientID: "testClient", - ClientSecret: "testSecret", - RedirectURI: ts.URL + "/callback", - Scopes: []string{"openid", "groups"}, - AdminEmail: "foo@bar.com", + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups"}, + DomainToAdminEmail: map[string]string{"*": "foo@bar.com"}, }, adc: serviceAccountFilePath, expectedErr: "", @@ -120,7 +147,7 @@ func TestOpen(t *testing.T) { ClientSecret: "testSecret", RedirectURI: ts.URL + "/callback", Scopes: []string{"openid", "groups"}, - AdminEmail: "foo@bar.com", + DomainToAdminEmail: map[string]string{"*": "foo@bar.com"}, ServiceAccountFilePath: serviceAccountFilePath, }, adc: "/dev/null", @@ -132,7 +159,7 @@ func TestOpen(t *testing.T) { assert := assert.New(t) os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", reference.adc) - conn, err := newConnector(reference.config, ts.URL) + conn, err := newConnector(reference.config) if reference.expectedErr == "" { assert.Nil(err) @@ -143,3 +170,282 @@ func TestOpen(t *testing.T) { }) } } + +func TestGetGroups(t *testing.T) { + ts := testSetup() + defer ts.Close() + + serviceAccountFilePath, err := tempServiceAccountKey() + assert.Nil(t, err) + + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFilePath) + conn, err := newConnector(&Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups"}, + DomainToAdminEmail: map[string]string{"*": "admin@dexidp.com"}, + }) + assert.Nil(t, err) + + conn.adminSrv[wildcardDomainToAdminEmail], err = admin.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(ts.URL)) + assert.Nil(t, err) + type testCase struct { + userKey string + fetchTransitiveGroupMembership bool + shouldErr bool + expectedGroups []string + } + + for name, testCase := range map[string]testCase{ + "user1_non_transitive_lookup": { + userKey: "user_1@dexidp.com", + fetchTransitiveGroupMembership: false, + shouldErr: false, + expectedGroups: []string{"groups_1@dexidp.com", "groups_2@dexidp.com"}, + }, + "user1_transitive_lookup": { + userKey: "user_1@dexidp.com", + fetchTransitiveGroupMembership: true, + shouldErr: false, + expectedGroups: []string{"groups_0@dexidp.com", "groups_1@dexidp.com", "groups_2@dexidp.com"}, + }, + "user2_non_transitive_lookup": { + userKey: "user_2@dexidp.com", + fetchTransitiveGroupMembership: false, + shouldErr: false, + expectedGroups: []string{"groups_1@dexidp.com"}, + }, + "user2_transitive_lookup": { + userKey: "user_2@dexidp.com", + fetchTransitiveGroupMembership: true, + shouldErr: false, + expectedGroups: []string{"groups_0@dexidp.com", "groups_1@dexidp.com"}, + }, + } { + testCase := testCase + callCounter = map[string]int{} + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + lookup := make(map[string]struct{}) + + groups, err := conn.getGroups(testCase.userKey, testCase.fetchTransitiveGroupMembership, lookup) + if testCase.shouldErr { + assert.NotNil(err) + } else { + assert.Nil(err) + } + assert.ElementsMatch(testCase.expectedGroups, groups) + t.Logf("[%s] Amount of API calls per userKey: %+v\n", t.Name(), callCounter) + }) + } +} + +func TestDomainToAdminEmailConfig(t *testing.T) { + ts := testSetup() + defer ts.Close() + + serviceAccountFilePath, err := tempServiceAccountKey() + assert.Nil(t, err) + + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFilePath) + conn, err := newConnector(&Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups"}, + DomainToAdminEmail: map[string]string{"dexidp.com": "admin@dexidp.com"}, + }) + assert.Nil(t, err) + + conn.adminSrv["dexidp.com"], err = admin.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(ts.URL)) + assert.Nil(t, err) + type testCase struct { + userKey string + expectedErr string + } + + for name, testCase := range map[string]testCase{ + "correct_user_request": { + userKey: "user_1@dexidp.com", + expectedErr: "", + }, + "wrong_user_request": { + userKey: "user_1@foo.bar", + expectedErr: "unable to find super admin email", + }, + "wrong_connector_response": { + userKey: "user_1_foo.bar", + expectedErr: "unable to find super admin email", + }, + } { + testCase := testCase + callCounter = map[string]int{} + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + lookup := make(map[string]struct{}) + + _, err := conn.getGroups(testCase.userKey, true, lookup) + if testCase.expectedErr != "" { + assert.ErrorContains(err, testCase.expectedErr) + } else { + assert.Nil(err) + } + t.Logf("[%s] Amount of API calls per userKey: %+v\n", t.Name(), callCounter) + }) + } +} + +var gceMetadataFlags = map[string]bool{ + "failOnEmailRequest": false, +} + +func mockGCEMetadataServer() *httptest.Server { + mux := http.NewServeMux() + + mux.HandleFunc("/computeMetadata/v1/instance/service-accounts/default/email", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + if gceMetadataFlags["failOnEmailRequest"] { + w.WriteHeader(http.StatusBadRequest) + } + json.NewEncoder(w).Encode("my-service-account@example-project.iam.gserviceaccount.com") + }) + mux.HandleFunc("/computeMetadata/v1/instance/service-accounts/default/token", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(struct { + AccessToken string `json:"access_token"` + ExpiresInSec int `json:"expires_in"` + TokenType string `json:"token_type"` + }{ + AccessToken: "my-example.token", + ExpiresInSec: 3600, + TokenType: "Bearer", + }) + }) + + return httptest.NewServer(mux) +} + +func TestGCEWorkloadIdentity(t *testing.T) { + ts := testSetup() + defer ts.Close() + + metadataServer := mockGCEMetadataServer() + defer metadataServer.Close() + metadataServerHost := strings.Replace(metadataServer.URL, "http://", "", 1) + + os.Setenv("GCE_METADATA_HOST", metadataServerHost) + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "") + os.Setenv("HOME", "/tmp") + + gceMetadataFlags["failOnEmailRequest"] = true + _, err := newConnector(&Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups"}, + DomainToAdminEmail: map[string]string{"dexidp.com": "admin@dexidp.com"}, + }) + assert.Error(t, err) + + gceMetadataFlags["failOnEmailRequest"] = false + conn, err := newConnector(&Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups"}, + DomainToAdminEmail: map[string]string{"dexidp.com": "admin@dexidp.com"}, + }) + assert.Nil(t, err) + + conn.adminSrv["dexidp.com"], err = admin.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(ts.URL)) + assert.Nil(t, err) + type testCase struct { + userKey string + expectedErr string + } + + for name, testCase := range map[string]testCase{ + "correct_user_request": { + userKey: "user_1@dexidp.com", + expectedErr: "", + }, + "wrong_user_request": { + userKey: "user_1@foo.bar", + expectedErr: "unable to find super admin email", + }, + "wrong_connector_response": { + userKey: "user_1_foo.bar", + expectedErr: "unable to find super admin email", + }, + } { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + lookup := make(map[string]struct{}) + + _, err := conn.getGroups(testCase.userKey, true, lookup) + if testCase.expectedErr != "" { + assert.ErrorContains(err, testCase.expectedErr) + } else { + assert.Nil(err) + } + }) + } +} + +func TestPromptTypeConfig(t *testing.T) { + promptTypeLogin := "login" + cases := []struct { + name string + promptType *string + expectedPromptTypeValue string + }{ + { + name: "prompt type is nil", + promptType: nil, + expectedPromptTypeValue: "consent", + }, + { + name: "prompt type is empty", + promptType: new(string), + expectedPromptTypeValue: "", + }, + { + name: "prompt type is set", + promptType: &promptTypeLogin, + expectedPromptTypeValue: "login", + }, + } + + ts := testSetup() + defer ts.Close() + + serviceAccountFilePath, err := tempServiceAccountKey() + assert.Nil(t, err) + + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFilePath) + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + conn, err := newConnector(&Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: ts.URL + "/callback", + Scopes: []string{"openid", "groups", "offline_access"}, + DomainToAdminEmail: map[string]string{"dexidp.com": "admin@dexidp.com"}, + PromptType: test.promptType, + }) + + assert.Nil(t, err) + assert.Equal(t, test.expectedPromptTypeValue, conn.promptType) + + loginURL, _, err := conn.LoginURL(connector.Scopes{OfflineAccess: true}, ts.URL+"/callback", "state") + assert.Nil(t, err) + + urlp, err := url.Parse(loginURL) + assert.Nil(t, err) + + assert.Equal(t, test.expectedPromptTypeValue, urlp.Query().Get("prompt")) + }) + } +} diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go index db97b5a71f..7d3084b238 100644 --- a/connector/keystone/keystone.go +++ b/connector/keystone/keystone.go @@ -7,18 +7,26 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" + "github.com/google/uuid" + "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" +) + +var ( + _ connector.PasswordConnector = (*conn)(nil) + _ connector.RefreshConnector = (*conn)(nil) ) type conn struct { - Domain string + Domain domainKeystone Host string AdminUsername string AdminPassword string - Logger log.Logger + client *http.Client + Logger *slog.Logger } type userKeystone struct { @@ -28,13 +36,14 @@ type userKeystone struct { } type domainKeystone struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } // Config holds the configuration parameters for Keystone connector. // Keystone should expose API v3 // An example config: +// // connectors: // type: keystone // id: keystone @@ -69,13 +78,9 @@ type password struct { } type user struct { - Name string `json:"name"` - Domain domain `json:"domain"` - Password string `json:"password"` -} - -type domain struct { - ID string `json:"id"` + Name string `json:"name"` + Domain domainKeystone `json:"domain"` + Password string `json:"password"` } type token struct { @@ -103,19 +108,29 @@ type userResponse struct { } `json:"user"` } -var ( - _ connector.PasswordConnector = &conn{} - _ connector.RefreshConnector = &conn{} -) - // Open returns an authentication strategy using Keystone. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { + _, err := uuid.Parse(c.Domain) + var domain domainKeystone + // check if the supplied domain is a UUID or the special "default" value + // which is treated as an ID and not a name + if err == nil || c.Domain == "default" { + domain = domainKeystone{ + ID: c.Domain, + } + } else { + domain = domainKeystone{ + Name: c.Domain, + } + } + return &conn{ - c.Domain, - c.Host, - c.AdminUsername, - c.AdminPassword, - logger, + Domain: domain, + Host: c.Host, + AdminUsername: c.AdminUsername, + AdminPassword: c.AdminPassword, + Logger: logger.With(slog.Group("connector", "type", "keystone", "id", id)), + client: http.DefaultClient, }, nil } @@ -192,7 +207,6 @@ func (p *conn) Refresh( } func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (response *http.Response, err error) { - client := &http.Client{} jsonData := loginRequestData{ auth: auth{ Identity: identity{ @@ -200,7 +214,7 @@ func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (res Password: password{ User: user{ Name: username, - Domain: domain{ID: p.Domain}, + Domain: p.Domain, Password: pass, }, }, @@ -221,7 +235,7 @@ func (p *conn) getTokenResponse(ctx context.Context, username, pass string) (res req.Header.Set("Content-Type", "application/json") req = req.WithContext(ctx) - return client.Do(req) + return p.client.Do(req) } func (p *conn) getAdminToken(ctx context.Context) (string, error) { @@ -243,7 +257,6 @@ func (p *conn) checkIfUserExists(ctx context.Context, userID string, token strin func (p *conn) getUser(ctx context.Context, userID string, token string) (*userResponse, error) { // https://developer.openstack.org/api-ref/identity/v3/#show-user-details userURL := p.Host + "/v3/users/" + userID - client := &http.Client{} req, err := http.NewRequest("GET", userURL, nil) if err != nil { return nil, err @@ -251,7 +264,7 @@ func (p *conn) getUser(ctx context.Context, userID string, token string) (*userR req.Header.Set("X-Auth-Token", token) req = req.WithContext(ctx) - resp, err := client.Do(req) + resp, err := p.client.Do(req) if err != nil { return nil, err } @@ -276,7 +289,6 @@ func (p *conn) getUser(ctx context.Context, userID string, token string) (*userR } func (p *conn) getUserGroups(ctx context.Context, userID string, token string) ([]string, error) { - client := &http.Client{} // https://developer.openstack.org/api-ref/identity/v3/#list-groups-to-which-a-user-belongs groupsURL := p.Host + "/v3/users/" + userID + "/groups" req, err := http.NewRequest("GET", groupsURL, nil) @@ -285,9 +297,9 @@ func (p *conn) getUserGroups(ctx context.Context, userID string, token string) ( } req.Header.Set("X-Auth-Token", token) req = req.WithContext(ctx) - resp, err := client.Do(req) + resp, err := p.client.Do(req) if err != nil { - p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID) + p.Logger.Error("error while fetching user groups", "user_id", userID, "err", err) return nil, err } diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go index fc6c01e229..9b0590df12 100644 --- a/connector/keystone/keystone_test.go +++ b/connector/keystone/keystone_test.go @@ -17,11 +17,13 @@ import ( const ( invalidPass = "WRONG_PASS" - testUser = "test_user" - testPass = "test_pass" - testEmail = "test@example.com" - testGroup = "test_group" - testDomain = "default" + testUser = "test_user" + testPass = "test_pass" + testEmail = "test@example.com" + testGroup = "test_group" + testDomainAltName = "altdomain" + testDomainID = "default" + testDomainName = "Default" ) var ( @@ -32,8 +34,26 @@ var ( authTokenURL = "" usersURL = "" groupsURL = "" + domainsURL = "" ) +type userReq struct { + Name string `json:"name"` + Email string `json:"email"` + Enabled bool `json:"enabled"` + Password string `json:"password"` + Roles []string `json:"roles"` + DomainID string `json:"domain_id,omitempty"` +} + +type domainResponse struct { + Domain domainKeystone `json:"domain"` +} + +type domainsResponse struct { + Domains []domainKeystone `json:"domains"` +} + type groupResponse struct { Group struct { ID string `json:"id"` @@ -42,8 +62,6 @@ type groupResponse struct { func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) { t.Helper() - client := &http.Client{} - jsonData := loginRequestData{ auth: auth{ Identity: identity{ @@ -51,7 +69,7 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) Password: password{ User: user{ Name: adminName, - Domain: domain{ID: testDomain}, + Domain: domainKeystone{ID: testDomainID}, Password: adminPass, }, }, @@ -70,7 +88,7 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) } req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } @@ -91,17 +109,91 @@ func getAdminToken(t *testing.T, adminName, adminPass string) (token, id string) return token, tokenResp.Token.User.ID } -func createUser(t *testing.T, token, userName, userEmail, userPass string) string { +func getOrCreateDomain(t *testing.T, token, domainName string) string { + t.Helper() + + domainSearchURL := domainsURL + "?name=" + domainName + reqGet, err := http.NewRequest("GET", domainSearchURL, nil) + if err != nil { + t.Fatal(err) + } + + reqGet.Header.Set("X-Auth-Token", token) + reqGet.Header.Add("Content-Type", "application/json") + respGet, err := http.DefaultClient.Do(reqGet) + if err != nil { + t.Fatal(err) + } + + dataGet, err := io.ReadAll(respGet.Body) + if err != nil { + t.Fatal(err) + } + defer respGet.Body.Close() + + domainsResp := new(domainsResponse) + err = json.Unmarshal(dataGet, &domainsResp) + if err != nil { + t.Fatal(err) + } + + if len(domainsResp.Domains) >= 1 { + return domainsResp.Domains[0].ID + } + + createDomainData := map[string]interface{}{ + "domain": map[string]interface{}{ + "name": domainName, + "enabled": true, + }, + } + + body, err := json.Marshal(createDomainData) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("POST", domainsURL, bytes.NewBuffer(body)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("X-Auth-Token", token) + req.Header.Add("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != 201 { + t.Fatalf("failed to create domain %s", domainName) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + domainResp := new(domainResponse) + err = json.Unmarshal(data, &domainResp) + if err != nil { + t.Fatal(err) + } + + return domainResp.Domain.ID +} + +func createUser(t *testing.T, token, domainID, userName, userEmail, userPass string) string { t.Helper() - client := &http.Client{} createUserData := map[string]interface{}{ - "user": map[string]interface{}{ - "name": userName, - "email": userEmail, - "enabled": true, - "password": userPass, - "roles": []string{"admin"}, + "user": userReq{ + DomainID: domainID, + Name: userName, + Email: userEmail, + Enabled: true, + Password: userPass, + Roles: []string{"admin"}, }, } @@ -116,7 +208,7 @@ func createUser(t *testing.T, token, userName, userEmail, userPass string) strin } req.Header.Set("X-Auth-Token", token) req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } @@ -139,7 +231,6 @@ func createUser(t *testing.T, token, userName, userEmail, userPass string) strin // delete group or user func deleteResource(t *testing.T, token, id, uri string) { t.Helper() - client := &http.Client{} deleteURI := uri + id req, err := http.NewRequest("DELETE", deleteURI, nil) @@ -148,7 +239,7 @@ func deleteResource(t *testing.T, token, id, uri string) { } req.Header.Set("X-Auth-Token", token) - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("error: %v", err) } @@ -157,7 +248,6 @@ func deleteResource(t *testing.T, token, id, uri string) { func createGroup(t *testing.T, token, description, name string) string { t.Helper() - client := &http.Client{} createGroupData := map[string]interface{}{ "group": map[string]interface{}{ @@ -177,7 +267,7 @@ func createGroup(t *testing.T, token, description, name string) string { } req.Header.Set("X-Auth-Token", token) req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } @@ -200,14 +290,13 @@ func createGroup(t *testing.T, token, description, name string) string { func addUserToGroup(t *testing.T, token, groupID, userID string) error { t.Helper() uri := groupsURL + groupID + "/users/" + userID - client := &http.Client{} req, err := http.NewRequest("PUT", uri, nil) if err != nil { return err } req.Header.Set("X-Auth-Token", token) - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("error: %v", err) } @@ -219,7 +308,8 @@ func addUserToGroup(t *testing.T, token, groupID, userID string) error { func TestIncorrectCredentialsLogin(t *testing.T) { setupVariables(t) c := conn{ - Host: keystoneURL, Domain: testDomain, + client: http.DefaultClient, + Host: keystoneURL, Domain: domainKeystone{ID: testDomainID}, AdminUsername: adminUser, AdminPassword: adminPass, } s := connector.Scopes{OfflineAccess: true, Groups: true} @@ -243,10 +333,11 @@ func TestValidUserLogin(t *testing.T) { token, _ := getAdminToken(t, adminUser, adminPass) type tUser struct { - username string - domain string - email string - password string + createDomain bool + domain domainKeystone + username string + email string + password string } type expect struct { @@ -263,10 +354,11 @@ func TestValidUserLogin(t *testing.T) { { name: "test with email address", input: tUser{ - username: testUser, - domain: testDomain, - email: testEmail, - password: testPass, + createDomain: false, + domain: domainKeystone{ID: testDomainID}, + username: testUser, + email: testEmail, + password: testPass, }, expected: expect{ username: testUser, @@ -277,10 +369,11 @@ func TestValidUserLogin(t *testing.T) { { name: "test without email address", input: tUser{ - username: testUser, - domain: testDomain, - email: "", - password: testPass, + createDomain: false, + domain: domainKeystone{ID: testDomainID}, + username: testUser, + email: "", + password: testPass, }, expected: expect{ username: testUser, @@ -288,21 +381,77 @@ func TestValidUserLogin(t *testing.T) { verifiedEmail: false, }, }, + { + name: "test with default domain Name", + input: tUser{ + createDomain: false, + domain: domainKeystone{Name: testDomainName}, + username: testUser, + email: testEmail, + password: testPass, + }, + expected: expect{ + username: testUser, + email: testEmail, + verifiedEmail: true, + }, + }, + { + name: "test with custom domain Name", + input: tUser{ + createDomain: true, + domain: domainKeystone{Name: testDomainAltName}, + username: testUser, + email: testEmail, + password: testPass, + }, + expected: expect{ + username: testUser, + email: testEmail, + verifiedEmail: true, + }, + }, + { + name: "test with custom domain ID", + input: tUser{ + createDomain: true, + domain: domainKeystone{}, + username: testUser, + email: testEmail, + password: testPass, + }, + expected: expect{ + username: testUser, + email: testEmail, + verifiedEmail: true, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - userID := createUser(t, token, tt.input.username, tt.input.email, tt.input.password) + domainID := "" + if tt.input.createDomain == true { + domainID = getOrCreateDomain(t, token, testDomainAltName) + t.Logf("getOrCreateDomain ID: %s\n", domainID) + + // if there was nothing set then use the dynamically generated domain ID + if tt.input.domain.ID == "" && tt.input.domain.Name == "" { + tt.input.domain.ID = domainID + } + } + userID := createUser(t, token, domainID, tt.input.username, tt.input.email, tt.input.password) defer deleteResource(t, token, userID, usersURL) c := conn{ - Host: keystoneURL, Domain: tt.input.domain, + client: http.DefaultClient, + Host: keystoneURL, Domain: tt.input.domain, AdminUsername: adminUser, AdminPassword: adminPass, } s := connector.Scopes{OfflineAccess: true, Groups: true} identity, validPW, err := c.Login(context.Background(), s, tt.input.username, tt.input.password) if err != nil { - t.Fatal(err.Error()) + t.Fatalf("Login failed for user %s: %v", tt.input.username, err.Error()) } t.Log(identity) if identity.Username != tt.expected.username { @@ -333,7 +482,8 @@ func TestUseRefreshToken(t *testing.T) { defer deleteResource(t, token, groupID, groupsURL) c := conn{ - Host: keystoneURL, Domain: testDomain, + client: http.DefaultClient, + Host: keystoneURL, Domain: domainKeystone{ID: testDomainID}, AdminUsername: adminUser, AdminPassword: adminPass, } s := connector.Scopes{OfflineAccess: true, Groups: true} @@ -355,10 +505,11 @@ func TestUseRefreshToken(t *testing.T) { func TestUseRefreshTokenUserDeleted(t *testing.T) { setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) - userID := createUser(t, token, testUser, testEmail, testPass) + userID := createUser(t, token, "", testUser, testEmail, testPass) c := conn{ - Host: keystoneURL, Domain: testDomain, + client: http.DefaultClient, + Host: keystoneURL, Domain: domainKeystone{ID: testDomainID}, AdminUsername: adminUser, AdminPassword: adminPass, } s := connector.Scopes{OfflineAccess: true, Groups: true} @@ -384,11 +535,12 @@ func TestUseRefreshTokenUserDeleted(t *testing.T) { func TestUseRefreshTokenGroupsChanged(t *testing.T) { setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) - userID := createUser(t, token, testUser, testEmail, testPass) + userID := createUser(t, token, "", testUser, testEmail, testPass) defer deleteResource(t, token, userID, usersURL) c := conn{ - Host: keystoneURL, Domain: testDomain, + client: http.DefaultClient, + Host: keystoneURL, Domain: domainKeystone{ID: testDomainID}, AdminUsername: adminUser, AdminPassword: adminPass, } s := connector.Scopes{OfflineAccess: true, Groups: true} @@ -420,11 +572,12 @@ func TestUseRefreshTokenGroupsChanged(t *testing.T) { func TestNoGroupsInScope(t *testing.T) { setupVariables(t) token, _ := getAdminToken(t, adminUser, adminPass) - userID := createUser(t, token, testUser, testEmail, testPass) + userID := createUser(t, token, "", testUser, testEmail, testPass) defer deleteResource(t, token, userID, usersURL) c := conn{ - Host: keystoneURL, Domain: testDomain, + client: http.DefaultClient, + Host: keystoneURL, Domain: domainKeystone{ID: testDomainID}, AdminUsername: adminUser, AdminPassword: adminPass, } s := connector.Scopes{OfflineAccess: true, Groups: false} @@ -474,6 +627,7 @@ func setupVariables(t *testing.T) { authTokenURL = keystoneURL + "/v3/auth/tokens/" usersURL = keystoneAdminURL + "/v3/users/" groupsURL = keystoneAdminURL + "/v3/groups/" + domainsURL = keystoneAdminURL + "/v3/domains/" } func expectEquals(t *testing.T, a interface{}, b interface{}) { diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 543402718c..e01e2df8c1 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -7,13 +7,15 @@ import ( "crypto/x509" "encoding/json" "fmt" + "log/slog" "net" + "net/url" "os" + "strings" "github.com/go-ldap/ldap/v3" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" ) // Config holds the configuration parameters for the LDAP connector. The LDAP @@ -32,10 +34,12 @@ import ( // bindDN: uid=serviceaccount,cn=users,dc=example,dc=com // bindPW: password // userSearch: -// # Would translate to the query "(&(objectClass=person)(uid=))" +// # Would translate to the query "(&(objectClass=person)(|(uid=)(mail=)))" // baseDN: cn=users,dc=example,dc=com // filter: "(objectClass=person)" -// username: uid +// username: +// - uid +// - mail // idAttr: uid // emailAttr: mail // nameAttr: name @@ -56,10 +60,33 @@ import ( // nameAttr: name // +// UsernameAttributes represents one or more LDAP attributes to match against +// the username input. It supports unmarshaling from both a single string +// (e.g. "uid") and a list of strings (e.g. ["uid", "mail"]). +type UsernameAttributes []string + +func (u *UsernameAttributes) UnmarshalJSON(data []byte) error { + var arr []string + if err := json.Unmarshal(data, &arr); err == nil { + *u = arr + return nil + } + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("username must be a string or list of strings") + } + if s != "" { + *u = UsernameAttributes{s} + } + return nil +} + // UserMatcher holds information about user and group matching. type UserMatcher struct { UserAttr string `json:"userAttr"` GroupAttr string `json:"groupAttr"` + // Look for parent groups + RecursionGroupAttr string `json:"recursionGroupAttr"` } // Config holds configuration options for LDAP logins. @@ -106,9 +133,10 @@ type Config struct { // Optional filter to apply when searching the directory. For example "(objectClass=person)" Filter string `json:"filter"` - // Attribute to match against the inputted username. This will be translated and combined - // with the other filter as "(=)". - Username string `json:"username"` + // Attribute(s) to match against the inputted username. Accepts a single string + // or a list of strings. When multiple attributes are specified, an OR filter is + // constructed: "(|(=)(=))". + Username UsernameAttributes `json:"username"` // Can either be: // * "sub" - search the whole sub tree @@ -142,6 +170,8 @@ type Config struct { UserAttr string `json:"userAttr"` GroupAttr string `json:"groupAttr"` + RecursionGroupAttr string `json:"recursionGroupAttr"` + // Array of the field pairs used to match a user to a group. // See the "UserMatcher" struct for the exact field names // @@ -187,22 +217,26 @@ func parseScope(s string) (int, bool) { // Function exists here to allow backward compatibility between old and new // group to user matching implementations. // See "Config.GroupSearch.UserMatchers" comments for the details -func userMatchers(c *Config, logger log.Logger) []UserMatcher { +func userMatchers(c *Config, logger *slog.Logger) []UserMatcher { if len(c.GroupSearch.UserMatchers) > 0 && c.GroupSearch.UserMatchers[0].UserAttr != "" { return c.GroupSearch.UserMatchers } - log.Deprecated(logger, `LDAP: use groupSearch.userMatchers option instead of "userAttr/groupAttr" fields.`) + if c.GroupSearch.UserAttr != "" || c.GroupSearch.GroupAttr != "" { + logger.Warn(`use "groupSearch.userMatchers" option instead of "userAttr/groupAttr" fields`, "deprecated", true) + } return []UserMatcher{ { - UserAttr: c.GroupSearch.UserAttr, - GroupAttr: c.GroupSearch.GroupAttr, + UserAttr: c.GroupSearch.UserAttr, + GroupAttr: c.GroupSearch.GroupAttr, + RecursionGroupAttr: c.GroupSearch.RecursionGroupAttr, }, } } // Open returns an authentication strategy using LDAP. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { + logger = logger.With(slog.Group("connector", "type", "ldap", "id", id)) conn, err := c.OpenConnector(logger) if err != nil { return nil, err @@ -216,7 +250,7 @@ type refreshData struct { } // OpenConnector is the same as Open but returns a type with all implemented connector interfaces. -func (c *Config) OpenConnector(logger log.Logger) (interface { +func (c *Config) OpenConnector(logger *slog.Logger) (interface { connector.Connector connector.PasswordConnector connector.RefreshConnector @@ -225,14 +259,13 @@ func (c *Config) OpenConnector(logger log.Logger) (interface { return c.openConnector(logger) } -func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) { +func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) { requiredFields := []struct { name string val string }{ {"host", c.Host}, {"userSearch.baseDN", c.UserSearch.BaseDN}, - {"userSearch.username", c.UserSearch.Username}, } for _, field := range requiredFields { @@ -241,6 +274,10 @@ func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) { } } + if len(c.UserSearch.Username) == 0 { + return nil, fmt.Errorf("ldap: missing required field %q", "userSearch.username") + } + var ( host string err error @@ -288,9 +325,14 @@ func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) { // TODO(nabokihms): remove it after deleting deprecated groupSearch options c.GroupSearch.UserMatchers = userMatchers(c, logger) - return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger}, nil + return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, c.UserSearch.Username, logger}, nil } +var ( + _ connector.PasswordConnector = (*ldapConnector)(nil) + _ connector.RefreshConnector = (*ldapConnector)(nil) +) + type ldapConnector struct { Config @@ -299,13 +341,10 @@ type ldapConnector struct { tlsConfig *tls.Config - logger log.Logger -} + usernameAttrs []string -var ( - _ connector.PasswordConnector = (*ldapConnector)(nil) - _ connector.RefreshConnector = (*ldapConnector)(nil) -) + logger *slog.Logger +} // do initializes a connection to the LDAP directory and passes it to the // provided function. It then performs appropriate teardown or reuse before @@ -316,11 +355,14 @@ func (c *ldapConnector) do(_ context.Context, f func(c *ldap.Conn) error) error conn *ldap.Conn err error ) + switch { case c.InsecureNoSSL: - conn, err = ldap.Dial("tcp", c.Host) + u := url.URL{Scheme: "ldap", Host: c.Host} + conn, err = ldap.DialURL(u.String()) case c.StartTLS: - conn, err = ldap.Dial("tcp", c.Host) + u := url.URL{Scheme: "ldap", Host: c.Host} + conn, err = ldap.DialURL(u.String()) if err != nil { return fmt.Errorf("failed to connect: %v", err) } @@ -328,7 +370,8 @@ func (c *ldapConnector) do(_ context.Context, f func(c *ldap.Conn) error) error return fmt.Errorf("start TLS failed: %v", err) } default: - conn, err = ldap.DialTLS("tcp", c.Host, c.tlsConfig) + u := url.URL{Scheme: "ldaps", Host: c.Host} + conn, err = ldap.DialURL(u.String(), ldap.DialWithTLSConfig(c.tlsConfig)) } if err != nil { return fmt.Errorf("failed to connect: %v", err) @@ -347,21 +390,23 @@ func (c *ldapConnector) do(_ context.Context, f func(c *ldap.Conn) error) error return f(conn) } -func getAttrs(e ldap.Entry, name string) []string { +func (c *ldapConnector) getAttrs(e ldap.Entry, name string) []string { for _, a := range e.Attributes { if a.Name != name { continue } return a.Values } - if name == "DN" { + if strings.ToLower(name) == "dn" { return []string{e.DN} } + + c.logger.Debug("attribute is not fround in entry", "attribute", name) return nil } -func getAttr(e ldap.Entry, name string) string { - if a := getAttrs(e, name); len(a) > 0 { +func (c *ldapConnector) getAttr(e ldap.Entry, name string) string { + if a := c.getAttrs(e, name); len(a) > 0 { return a[0] } return "" @@ -373,25 +418,25 @@ func (c *ldapConnector) identityFromEntry(user ldap.Entry) (ident connector.Iden missing := []string{} // Fill the identity struct using the attributes from the user entry. - if ident.UserID = getAttr(user, c.UserSearch.IDAttr); ident.UserID == "" { + if ident.UserID = c.getAttr(user, c.UserSearch.IDAttr); ident.UserID == "" { missing = append(missing, c.UserSearch.IDAttr) } if c.UserSearch.NameAttr != "" { - if ident.Username = getAttr(user, c.UserSearch.NameAttr); ident.Username == "" { + if ident.Username = c.getAttr(user, c.UserSearch.NameAttr); ident.Username == "" { missing = append(missing, c.UserSearch.NameAttr) } } if c.UserSearch.PreferredUsernameAttrAttr != "" { - if ident.PreferredUsername = getAttr(user, c.UserSearch.PreferredUsernameAttrAttr); ident.PreferredUsername == "" { + if ident.PreferredUsername = c.getAttr(user, c.UserSearch.PreferredUsernameAttrAttr); ident.PreferredUsername == "" { missing = append(missing, c.UserSearch.PreferredUsernameAttrAttr) } } if c.UserSearch.EmailSuffix != "" { ident.Email = ident.Username + "@" + c.UserSearch.EmailSuffix - } else if ident.Email = getAttr(user, c.UserSearch.EmailAttr); ident.Email == "" { + } else if ident.Email = c.getAttr(user, c.UserSearch.EmailAttr); ident.Email == "" { missing = append(missing, c.UserSearch.EmailAttr) } // TODO(ericchiang): Let this value be set from an attribute. @@ -405,7 +450,19 @@ func (c *ldapConnector) identityFromEntry(user ldap.Entry) (ident connector.Iden } func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.Entry, found bool, err error) { - filter := fmt.Sprintf("(%s=%s)", c.UserSearch.Username, ldap.EscapeFilter(username)) + var filter string + escapedUsername := ldap.EscapeFilter(username) + + attrFilters := make([]string, 0, len(c.usernameAttrs)) + for _, attr := range c.usernameAttrs { + attrFilters = append(attrFilters, fmt.Sprintf("(%s=%s)", attr, escapedUsername)) + } + if len(attrFilters) == 1 { + filter = attrFilters[0] // Skip OR wrapper for single attribute + } else { + filter = fmt.Sprintf("(|%s)", strings.Join(attrFilters, "")) + } + if c.UserSearch.Filter != "" { filter = fmt.Sprintf("(&%s%s)", c.UserSearch.Filter, filter) } @@ -423,6 +480,8 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E }, } + req.Attributes = append(req.Attributes, c.usernameAttrs...) + for _, matcher := range c.GroupSearch.UserMatchers { req.Attributes = append(req.Attributes, matcher.UserAttr) } @@ -435,8 +494,8 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E req.Attributes = append(req.Attributes, c.UserSearch.PreferredUsernameAttrAttr) } - c.logger.Infof("performing ldap search %s %s %s", - req.BaseDN, scopeString(req.Scope), req.Filter) + c.logger.Info("performing ldap search", + "base_dn", req.BaseDN, "scope", scopeString(req.Scope), "filter", req.Filter) resp, err := conn.Search(req) if err != nil { return ldap.Entry{}, false, fmt.Errorf("ldap: search with filter %q failed: %v", req.Filter, err) @@ -444,11 +503,11 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E switch n := len(resp.Entries); n { case 0: - c.logger.Errorf("ldap: no results returned for filter: %q", filter) + c.logger.Error("no results returned for filter", "filter", filter) return ldap.Entry{}, false, nil case 1: user = *resp.Entries[0] - c.logger.Infof("username %q mapped to entry %s", username, user.DN) + c.logger.Info("username mapped to entry", "username", username, "user_dn", user.DN) return user, true, nil default: return ldap.Entry{}, false, fmt.Errorf("ldap: filter returned multiple (%d) results: %q", n, filter) @@ -457,6 +516,7 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) { // make this check to avoid unauthenticated bind to the LDAP server. + if password == "" { return connector.Identity{}, false, nil } @@ -468,6 +528,8 @@ func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username, user ldap.Entry ) + username = ldap.EscapeFilter(username) + err = c.do(ctx, func(conn *ldap.Conn) error { entry, found, err := c.userEntry(conn, username) if err != nil { @@ -485,11 +547,11 @@ func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username, if ldapErr, ok := err.(*ldap.Error); ok { switch ldapErr.ResultCode { case ldap.LDAPResultInvalidCredentials: - c.logger.Errorf("ldap: invalid password for user %q", user.DN) + c.logger.Error("invalid password for user", "user_dn", user.DN) incorrectPass = true return nil case ldap.LDAPResultConstraintViolation: - c.logger.Errorf("ldap: constraint violation for user %q: %s", user.DN, ldapErr.Error()) + c.logger.Error("constraint violation for user", "user_dn", user.DN, "err", ldapErr.Error()) incorrectPass = true return nil } @@ -575,61 +637,124 @@ func (c *ldapConnector) Refresh(ctx context.Context, s connector.Scopes, ident c func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string, error) { if c.GroupSearch.BaseDN == "" { - c.logger.Debugf("No groups returned for %q because no groups baseDN has been configured.", getAttr(user, c.UserSearch.NameAttr)) + c.logger.Debug("No groups returned because no groups baseDN has been configured.", "base_dn", c.getAttr(user, c.UserSearch.NameAttr)) return nil, nil } - var groups []*ldap.Entry + var groupNames []string + for _, matcher := range c.GroupSearch.UserMatchers { - for _, attr := range getAttrs(user, matcher.UserAttr) { - filter := fmt.Sprintf("(%s=%s)", matcher.GroupAttr, ldap.EscapeFilter(attr)) - if c.GroupSearch.Filter != "" { - filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter) + // Initial Search + var groups []*ldap.Entry + for _, attr := range c.getAttrs(user, matcher.UserAttr) { + obtained, filter, err := c.queryGroups(ctx, matcher.GroupAttr, attr) + if err != nil { + return nil, err + } + gotGroups := len(obtained) != 0 + if !gotGroups { + // TODO(ericchiang): Is this going to spam the logs? + c.logger.Error("ldap: groups search returned no groups", "filter", filter) } + groups = append(groups, obtained...) + } - req := &ldap.SearchRequest{ - BaseDN: c.GroupSearch.BaseDN, - Filter: filter, - Scope: c.groupSearchScope, - Attributes: []string{c.GroupSearch.NameAttr}, + // If RecursionGroupAttr is not set, convert direct groups into names and return + if matcher.RecursionGroupAttr == "" { + for _, group := range groups { + name := c.getAttr(*group, c.GroupSearch.NameAttr) + if name == "" { + return nil, fmt.Errorf( + "ldap: group entity %q missing required attribute %q", + group.DN, c.GroupSearch.NameAttr, + ) + } + groupNames = append(groupNames, name) } + continue + } + + // Recursive Search + c.logger.Info("Recursive group search enabled", "groupAttr", matcher.GroupAttr, "recursionAttr", matcher.RecursionGroupAttr) + for { + var nextLevel []*ldap.Entry + for _, group := range groups { + name := c.getAttr(*group, c.GroupSearch.NameAttr) + if name == "" { + return nil, fmt.Errorf("ldap: group entity %q missing required attribute %q", + group.DN, c.GroupSearch.NameAttr) + } + + // Prevent duplicates and circular references. + duplicate := false + for _, existingName := range groupNames { + if name == existingName { + c.logger.Debug("Found duplicate group", "name", name) + duplicate = true + break + } + } + if duplicate { + continue + } - gotGroups := false - if err := c.do(ctx, func(conn *ldap.Conn) error { - c.logger.Infof("performing ldap search %s %s %s", - req.BaseDN, scopeString(req.Scope), req.Filter) - resp, err := conn.Search(req) + groupNames = append(groupNames, name) + + // Search for parent groups using the group's DN. + parents, filter, err := c.queryGroups(ctx, matcher.RecursionGroupAttr, group.DN) if err != nil { - return fmt.Errorf("ldap: search failed: %v", err) + return nil, err + } + if len(parents) == 0 { + c.logger.Debug("No parent groups found", "filter", filter) + } else { + nextLevel = append(nextLevel, parents...) } - gotGroups = len(resp.Entries) != 0 - groups = append(groups, resp.Entries...) - return nil - }); err != nil { - return nil, err } - if !gotGroups { - // TODO(ericchiang): Is this going to spam the logs? - c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter) + if len(nextLevel) == 0 { + break } + groups = nextLevel } } + return groupNames, nil +} - groupNames := make([]string, 0, len(groups)) - for _, group := range groups { - name := getAttr(*group, c.GroupSearch.NameAttr) - if name == "" { - // Be obnoxious about missing missing attributes. If the group entry is - // missing its name attribute, that indicates a misconfiguration. - // - // In the future we can add configuration options to just log these errors. - return nil, fmt.Errorf("ldap: group entity %q missing required attribute %q", - group.DN, c.GroupSearch.NameAttr) - } +func (c *ldapConnector) queryGroups(ctx context.Context, memberAttr, dn string) ([]*ldap.Entry, string, error) { + filter := fmt.Sprintf("(%s=%s)", memberAttr, ldap.EscapeFilter(dn)) + if c.GroupSearch.Filter != "" { + filter = fmt.Sprintf("(&%s%s)", c.GroupSearch.Filter, filter) + } - groupNames = append(groupNames, name) + req := &ldap.SearchRequest{ + BaseDN: c.GroupSearch.BaseDN, + Filter: filter, + Scope: c.groupSearchScope, + Attributes: []string{c.GroupSearch.NameAttr}, + } + + var entries []*ldap.Entry + if err := c.do(ctx, func(conn *ldap.Conn) error { + c.logger.Info( + "performing ldap search", + "base_dn", req.BaseDN, + "scope", scopeString(req.Scope), + "filter", req.Filter, + ) + resp, err := conn.Search(req) + if err != nil { + if ldapErr, ok := err.(*ldap.Error); ok && ldapErr.ResultCode == ldap.LDAPResultNoSuchObject { + c.logger.Info("LDAP search returned no groups", "filter", filter) + return nil + } + return fmt.Errorf("ldap: search failed: %v", err) + } + entries = append(entries, resp.Entries...) + return nil + }); err != nil { + return nil, filter, err } - return groupNames, nil + return entries, filter, nil } func (c *ldapConnector) Prompt() string { diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go index 83f9f4790c..3335d56b5e 100644 --- a/connector/ldap/ldap_test.go +++ b/connector/ldap/ldap_test.go @@ -3,12 +3,11 @@ package ldap import ( "context" "fmt" - "io" + "log/slog" "os" "testing" "github.com/kylelemons/godebug/pretty" - "github.com/sirupsen/logrus" "github.com/dexidp/dex/connector" ) @@ -46,7 +45,7 @@ func TestQuery(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} tests := []subtest{ { @@ -83,6 +82,18 @@ func TestQuery(t *testing.T) { password: "foo", wantBadPW: true, // Want invalid password, not a query error. }, + { + name: "invalid wildcard username", + username: "a*", // wildcard query is not allowed + password: "foo", + wantBadPW: true, // Want invalid password, not a query error. + }, + { + name: "invalid wildcard password", + username: "john", + password: "*", // wildcard password is not allowed + wantBadPW: true, // Want invalid password, not a query error. + }, } runTests(t, connectLDAP, c, tests) @@ -94,7 +105,7 @@ func TestQueryWithEmailSuffix(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailSuffix = "test.example.com" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} tests := []subtest{ { @@ -130,7 +141,7 @@ func TestUserFilter(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} c.UserSearch.Filter = "(ou:dn:=Seattle)" tests := []subtest{ @@ -173,13 +184,50 @@ func TestUserFilter(t *testing.T) { runTests(t, connectLDAP, c, tests) } +func TestUsernameWithMultipleAttributes(t *testing.T) { + c := &Config{} + c.UserSearch.BaseDN = "ou=TestUsernameWithMultipleAttributes,dc=example,dc=org" + c.UserSearch.NameAttr = "cn" + c.UserSearch.EmailAttr = "mail" + c.UserSearch.IDAttr = "DN" + c.UserSearch.Username = UsernameAttributes{"cn", "mail"} + c.UserSearch.Filter = "(ou:dn:=Seattle)" + + tests := []subtest{ + { + name: "cn", + username: "jane", + password: "foo", + want: connector.Identity{ + UserID: "cn=jane,ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org", + Username: "jane", + Email: "janedoe@example.com", + EmailVerified: true, + }, + }, + { + name: "mail", + username: "janedoe@example.com", + password: "foo", + want: connector.Identity{ + UserID: "cn=jane,ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org", + Username: "jane", + Email: "janedoe@example.com", + EmailVerified: true, + }, + }, + } + + runTests(t, connectLDAP, c, tests) +} + func TestGroupQuery(t *testing.T) { c := &Config{} c.UserSearch.BaseDN = "ou=People,ou=TestGroupQuery,dc=example,dc=org" c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} c.GroupSearch.BaseDN = "ou=Groups,ou=TestGroupQuery,dc=example,dc=org" c.GroupSearch.UserMatchers = []UserMatcher{ { @@ -227,7 +275,7 @@ func TestGroupsOnUserEntity(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} c.GroupSearch.BaseDN = "ou=Groups,ou=TestGroupsOnUserEntity,dc=example,dc=org" c.GroupSearch.UserMatchers = []UserMatcher{ { @@ -273,11 +321,11 @@ func TestGroupFilter(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} c.GroupSearch.BaseDN = "ou=TestGroupFilter,dc=example,dc=org" c.GroupSearch.UserMatchers = []UserMatcher{ { - UserAttr: "DN", + UserAttr: "dn", GroupAttr: "member", }, } @@ -322,7 +370,7 @@ func TestGroupToUserMatchers(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} c.GroupSearch.BaseDN = "ou=TestGroupToUserMatchers,dc=example,dc=org" c.GroupSearch.UserMatchers = []UserMatcher{ { @@ -378,7 +426,7 @@ func TestDeprecatedGroupToUserMatcher(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} c.GroupSearch.BaseDN = "ou=TestDeprecatedGroupToUserMatcher,dc=example,dc=org" c.GroupSearch.UserAttr = "DN" c.GroupSearch.GroupAttr = "member" @@ -423,7 +471,7 @@ func TestStartTLS(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} tests := []subtest{ { @@ -447,7 +495,7 @@ func TestInsecureSkipVerify(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} tests := []subtest{ { @@ -471,7 +519,7 @@ func TestLDAPS(t *testing.T) { c.UserSearch.NameAttr = "cn" c.UserSearch.EmailAttr = "mail" c.UserSearch.IDAttr = "DN" - c.UserSearch.Username = "cn" + c.UserSearch.Username = UsernameAttributes{"cn"} tests := []subtest{ { @@ -514,6 +562,86 @@ func TestUsernamePrompt(t *testing.T) { } } +func TestUsernameAttributesUnmarshal(t *testing.T) { + tests := []struct { + name string + json string + want UsernameAttributes + wantErr bool + }{ + {name: "single string", json: `"uid"`, want: UsernameAttributes{"uid"}}, + {name: "array of strings", json: `["uid","mail"]`, want: UsernameAttributes{"uid", "mail"}}, + {name: "single element array", json: `["cn"]`, want: UsernameAttributes{"cn"}}, + {name: "empty string", json: `""`, want: nil}, + {name: "invalid type", json: `123`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got UsernameAttributes + err := got.UnmarshalJSON([]byte(tt.json)) + if (err != nil) != tt.wantErr { + t.Fatalf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr { + if diff := pretty.Compare(tt.want, got); diff != "" { + t.Errorf("unexpected result: %s", diff) + } + } + }) + } +} + +func TestNestedGroups(t *testing.T) { + c := &Config{} + c.UserSearch.BaseDN = "ou=People,ou=TestNestedGroups,dc=example,dc=org" + c.UserSearch.NameAttr = "cn" + c.UserSearch.EmailAttr = "mail" + c.UserSearch.IDAttr = "DN" + c.UserSearch.Username = UsernameAttributes{"cn"} + + c.GroupSearch.BaseDN = "ou=TestNestedGroups,dc=example,dc=org" + c.GroupSearch.UserMatchers = []UserMatcher{ + { + UserAttr: "DN", + GroupAttr: "member", + // Enable Recursive Search + RecursionGroupAttr: "member", + }, + } + c.GroupSearch.NameAttr = "cn" + + tests := []subtest{ + { + name: "nestedgroups_jane", + username: "jane", + password: "foo", + groups: true, + want: connector.Identity{ + UserID: "cn=jane,ou=People,ou=TestNestedGroups,dc=example,dc=org", + Username: "jane", + Email: "janedoe@example.com", + EmailVerified: true, + Groups: []string{"childGroup", "circularGroup1", "intermediateGroup", "circularGroup2", "parentGroup"}, + }, + }, + { + name: "nestedgroups_john", + username: "john", + password: "bar", + groups: true, + want: connector.Identity{ + UserID: "cn=john,ou=People,ou=TestNestedGroups,dc=example,dc=org", + Username: "john", + Email: "johndoe@example.com", + EmailVerified: true, + Groups: []string{"circularGroup2", "intermediateGroup", "circularGroup1", "parentGroup"}, + }, + }, + } + runTests(t, connectLDAP, c, tests) +} + func getenv(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val @@ -523,7 +651,7 @@ func getenv(key, defaultVal string) string { // runTests runs a set of tests against an LDAP schema. // -// The tests require LDAP to be runnning. +// The tests require LDAP to be running. // You can use the provided docker-compose file to setup an LDAP server. func runTests(t *testing.T, connMethod connectionMethod, config *Config, tests []subtest) { ldapHost := os.Getenv("DEX_LDAP_HOST") @@ -555,7 +683,7 @@ func runTests(t *testing.T, connMethod connectionMethod, config *Config, tests [ c.BindDN = "cn=admin,dc=example,dc=org" c.BindPW = "admin" - l := &logrus.Logger{Out: io.Discard, Formatter: &logrus.TextFormatter{}} + l := slog.New(slog.DiscardHandler) conn, err := c.openConnector(l) if err != nil { diff --git a/connector/ldap/testdata/schema.ldif b/connector/ldap/testdata/schema.ldif index 69c7b3ff64..ab133543b4 100644 --- a/connector/ldap/testdata/schema.ldif +++ b/connector/ldap/testdata/schema.ldif @@ -445,3 +445,85 @@ sn: doe cn: jane mail: janedoe@example.com userpassword: foo + +######################################################################## + +dn: ou=TestNestedGroups,dc=example,dc=org +objectClass: organizationalUnit +ou: TestNestedGroups + +dn: ou=People,ou=TestNestedGroups,dc=example,dc=org +objectClass: organizationalUnit +ou: People + +dn: cn=jane,ou=People,ou=TestNestedGroups,dc=example,dc=org +objectClass: person +objectClass: inetOrgPerson +sn: doe +cn: jane +mail: janedoe@example.com +userpassword: foo + +dn: cn=john,ou=People,ou=TestNestedGroups,dc=example,dc=org +objectClass: person +objectClass: inetOrgPerson +sn: doe +cn: john +mail: johndoe@example.com +userpassword: bar + +# Group definitions. + +dn: ou=Groups,ou=TestNestedGroups,dc=example,dc=org +objectClass: organizationalUnit +ou: Groups + +dn: cn=childGroup,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +objectClass: groupOfNames +cn: childGroup +member: cn=jane,ou=People,ou=TestNestedGroups,dc=example,dc=org + +dn: cn=intermediateGroup,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +objectClass: groupOfNames +cn: intermediateGroup +member: cn=childGroup,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +member: cn=john,ou=People,ou=TestNestedGroups,dc=example,dc=org + +dn: cn=parentGroup,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +objectClass: groupOfNames +cn: parentGroup +member: cn=intermediateGroup,ou=Groups,ou=TestNestedGroups,dc=example,dc=org + +dn: cn=circularGroup1,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +objectClass: groupOfNames +cn: circularGroup1 +member: cn=circularGroup2,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +member: cn=jane,ou=People,ou=TestNestedGroups,dc=example,dc=org + +dn: cn=circularGroup2,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +objectClass: groupOfNames +cn: circularGroup2 +member: cn=circularGroup1,ou=Groups,ou=TestNestedGroups,dc=example,dc=org +member: cn=john,ou=People,ou=TestNestedGroups,dc=example,dc=org + +######################################################################## + +dn: ou=TestUsernameWithMultipleAttributes,dc=example,dc=org +objectClass: organizationalUnit +ou: TestUsernameWithMultipleAttributes + +dn: ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org +objectClass: organizationalUnit +ou: Seattle + +dn: ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org +objectClass: organizationalUnit +ou: People + +dn: cn=jane,ou=People,ou=Seattle,ou=TestUsernameWithMultipleAttributes,dc=example,dc=org +objectClass: person +objectClass: inetOrgPerson +sn: doe +cn: jane +mail: janedoe@example.com +userpassword: foo \ No newline at end of file diff --git a/connector/linkedin/linkedin.go b/connector/linkedin/linkedin.go index f79f1c49d8..32e33aeaca 100644 --- a/connector/linkedin/linkedin.go +++ b/connector/linkedin/linkedin.go @@ -6,13 +6,13 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "strings" "golang.org/x/oauth2" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" ) const ( @@ -29,7 +29,7 @@ type Config struct { } // Open returns a strategy for logging in through LinkedIn -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { return &linkedInConnector{ oauth2Config: &oauth2.Config{ ClientID: c.ClientID, @@ -41,7 +41,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) Scopes: []string{"r_liteprofile", "r_emailaddress"}, RedirectURL: c.RedirectURI, }, - logger: logger, + logger: logger.With(slog.Group("connector", "type", "linkedin", "id", id)), }, nil } @@ -49,30 +49,28 @@ type connectorData struct { AccessToken string `json:"accessToken"` } -type linkedInConnector struct { - oauth2Config *oauth2.Config - logger log.Logger -} - -// LinkedIn doesn't provide refresh tokens, so refresh tokens issued by Dex -// will expire in 60 days (default LinkedIn token lifetime). var ( _ connector.CallbackConnector = (*linkedInConnector)(nil) _ connector.RefreshConnector = (*linkedInConnector)(nil) ) +type linkedInConnector struct { + oauth2Config *oauth2.Config + logger *slog.Logger +} + // LoginURL returns an access token request URL -func (c *linkedInConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *linkedInConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.oauth2Config.RedirectURL != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.oauth2Config.RedirectURL) } - return c.oauth2Config.AuthCodeURL(state), nil + return c.oauth2Config.AuthCodeURL(state), nil, nil } // HandleCallback handles HTTP redirect from LinkedIn -func (c *linkedInConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *linkedInConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go index 3952c94be6..ca6e025dcf 100644 --- a/connector/microsoft/microsoft.go +++ b/connector/microsoft/microsoft.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "strings" "sync" @@ -17,7 +18,6 @@ import ( "github.com/dexidp/dex/connector" groups_pkg "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" ) // GroupNameFormat represents the format of the group identifier @@ -54,6 +54,9 @@ type Config struct { UseGroupsAsWhitelist bool `json:"useGroupsAsWhitelist"` EmailToLowercase bool `json:"emailToLowercase"` + APIURL string `json:"apiURL"` + GraphURL string `json:"graphURL"` + // PromptType is used for the prompt query parameter. // For valid values, see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code. PromptType string `json:"promptType"` @@ -63,10 +66,10 @@ type Config struct { } // Open returns a strategy for logging in through Microsoft. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { m := microsoftConnector{ - apiURL: "https://login.microsoftonline.com", - graphURL: "https://graph.microsoft.com", + apiURL: strings.TrimSuffix(c.APIURL, "/"), + graphURL: strings.TrimSuffix(c.GraphURL, "/"), redirectURI: c.RedirectURI, clientID: c.ClientID, clientSecret: c.ClientSecret, @@ -75,12 +78,21 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) groups: c.Groups, groupNameFormat: c.GroupNameFormat, useGroupsAsWhitelist: c.UseGroupsAsWhitelist, - logger: logger, + logger: logger.With(slog.Group("connector", "type", "microsoft", "id", id)), emailToLowercase: c.EmailToLowercase, promptType: c.PromptType, domainHint: c.DomainHint, scopes: c.Scopes, } + + if m.apiURL == "" { + m.apiURL = "https://login.microsoftonline.com" + } + + if m.graphURL == "" { + m.graphURL = "https://graph.microsoft.com" + } + // By default allow logins from both personal and business/school // accounts. if m.tenant == "" { @@ -121,7 +133,7 @@ type microsoftConnector struct { groupNameFormat GroupNameFormat groups []string useGroupsAsWhitelist bool - logger log.Logger + logger *slog.Logger emailToLowercase bool promptType string domainHint string @@ -163,9 +175,9 @@ func (c *microsoftConnector) oauth2Config(scopes connector.Scopes) *oauth2.Confi } } -func (c *microsoftConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *microsoftConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } var options []oauth2.AuthCodeOption @@ -176,10 +188,10 @@ func (c *microsoftConnector) LoginURL(scopes connector.Scopes, callbackURL, stat options = append(options, oauth2.SetAuthURLParam("domain_hint", c.domainHint)) } - return c.oauth2Config(scopes).AuthCodeURL(state, options...), nil + return c.oauth2Config(scopes).AuthCodeURL(state, options...), nil, nil } -func (c *microsoftConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *microsoftConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} @@ -215,7 +227,7 @@ func (c *microsoftConnector) HandleCallback(s connector.Scopes, r *http.Request) if c.groupsRequired(s.Groups) { groups, err := c.getGroups(ctx, client, user.ID) if err != nil { - return identity, fmt.Errorf("microsoft: get groups: %v", err) + return identity, fmt.Errorf("microsoft: get groups: %w", err) } identity.Groups = groups } @@ -306,7 +318,7 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id if c.groupsRequired(s.Groups) { groups, err := c.getGroups(ctx, client, user.ID) if err != nil { - return identity, fmt.Errorf("microsoft: get groups: %v", err) + return identity, fmt.Errorf("microsoft: get groups: %w", err) } identity.Groups = groups } @@ -316,22 +328,27 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/user // id - The unique identifier for the user. Inherited from -// directoryObject. Key. Not nullable. Read-only. +// +// directoryObject. Key. Not nullable. Read-only. +// // displayName - The name displayed in the address book for the user. -// This is usually the combination of the user's first name, -// middle initial and last name. This property is required -// when a user is created and it cannot be cleared during -// updates. Supports $filter and $orderby. +// +// This is usually the combination of the user's first name, +// middle initial and last name. This property is required +// when a user is created and it cannot be cleared during +// updates. Supports $filter and $orderby. +// // userPrincipalName - The user principal name (UPN) of the user. -// The UPN is an Internet-style login name for the user -// based on the Internet standard RFC 822. By convention, -// this should map to the user's email name. The general -// format is alias@domain, where domain must be present in -// the tenant’s collection of verified domains. This -// property is required when a user is created. The -// verified domains for the tenant can be accessed from the -// verifiedDomains property of organization. Supports -// $filter and $orderby. +// +// The UPN is an Internet-style login name for the user +// based on the Internet standard RFC 822. By convention, +// this should map to the user's email name. The general +// format is alias@domain, where domain must be present in +// the tenant’s collection of verified domains. This +// property is required when a user is created. The +// verified domains for the tenant can be accessed from the +// verifiedDomains property of organization. Supports +// $filter and $orderby. type user struct { ID string `json:"id"` Name string `json:"displayName"` @@ -364,8 +381,9 @@ func (c *microsoftConnector) user(ctx context.Context, client *http.Client) (u u // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/group // displayName - The display name for the group. This property is required when -// a group is created and it cannot be cleared during updates. -// Supports $filter and $orderby. +// +// a group is created and it cannot be cleared during updates. +// Supports $filter and $orderby. type group struct { Name string `json:"displayName"` } @@ -386,7 +404,7 @@ func (c *microsoftConnector) getGroups(ctx context.Context, client *http.Client, // ensure that the user is in at least one required group filteredGroups := groups_pkg.Filter(userGroups, c.groups) if len(c.groups) > 0 && len(filteredGroups) == 0 { - return nil, fmt.Errorf("microsoft: user %v not in any of the required groups", userID) + return nil, &connector.UserNotInRequiredGroupsError{UserID: userID, Groups: c.groups} } else if c.useGroupsAsWhitelist { return filteredGroups, nil } diff --git a/connector/microsoft/microsoft_test.go b/connector/microsoft/microsoft_test.go index 67be660fce..f0dcd96ddc 100644 --- a/connector/microsoft/microsoft_test.go +++ b/connector/microsoft/microsoft_test.go @@ -2,6 +2,7 @@ package microsoft import ( "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -39,7 +40,7 @@ func TestLoginURL(t *testing.T) { tenant: tenant, } - loginURL, _ := conn.LoginURL(connector.Scopes{}, conn.redirectURI, testState) + loginURL, _, _ := conn.LoginURL(connector.Scopes{}, conn.redirectURI, testState) parsedLoginURL, _ := url.Parse(loginURL) queryParams := parsedLoginURL.Query() @@ -70,7 +71,7 @@ func TestLoginURLWithOptions(t *testing.T) { domainHint: domainHint, } - loginURL, _ := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") + loginURL, _, _ := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") parsedLoginURL, _ := url.Parse(loginURL) queryParams := parsedLoginURL.Query() @@ -91,7 +92,7 @@ func TestUserIdentityFromGraphAPI(t *testing.T) { req, _ := http.NewRequest("GET", s.URL, nil) c := microsoftConnector{apiURL: s.URL, graphURL: s.URL, tenant: tenant} - identity, err := c.HandleCallback(connector.Scopes{Groups: false}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: false}, nil, req) expectNil(t, err) expectEquals(t, identity.Username, "Jane Doe") expectEquals(t, identity.UserID, "S56767889") @@ -114,11 +115,44 @@ func TestUserGroupsFromGraphAPI(t *testing.T) { req, _ := http.NewRequest("GET", s.URL, nil) c := microsoftConnector{apiURL: s.URL, graphURL: s.URL, tenant: tenant} - identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.Groups, []string{"a", "b"}) } +func TestUserNotInRequiredGroupFromGraphAPI(t *testing.T) { + s := newTestServer(map[string]testResponse{ + "/v1.0/me?$select=id,displayName,userPrincipalName": { + data: user{ID: "user-id-123", Name: "Jane Doe", Email: "jane.doe@example.com"}, + }, + // The user is a member of groups "c" and "d", but the connector only + // allows group "a" — so the user should be denied. + "/v1.0/me/getMemberGroups": {data: map[string]interface{}{ + "value": []string{"c", "d"}, + }}, + "/" + tenant + "/oauth2/v2.0/token": dummyToken, + }) + defer s.Close() + + req, _ := http.NewRequest("GET", s.URL, nil) + + c := microsoftConnector{ + apiURL: s.URL, + graphURL: s.URL, + tenant: tenant, + groups: []string{"a"}, + } + _, err := c.HandleCallback(connector.Scopes{Groups: true}, nil, req) + if err == nil { + t.Fatal("expected error when user is not in any required group, got nil") + } + + var groupsErr *connector.UserNotInRequiredGroupsError + if !errors.As(err, &groupsErr) { + t.Errorf("expected *connector.UserNotInRequiredGroupsError, got %T: %v", err, err) + } +} + func newTestServer(responses map[string]testResponse) *httptest.Server { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { response, found := responses[r.RequestURI] diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go index e7ee438625..4d9e9e2707 100644 --- a/connector/mock/connectortest.go +++ b/connector/mock/connectortest.go @@ -5,16 +5,16 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "net/url" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" ) // NewCallbackConnector returns a mock connector which requires no user interaction. It always returns // the same (fake) identity. -func NewCallbackConnector(logger log.Logger) connector.Connector { +func NewCallbackConnector(logger *slog.Logger) connector.Connector { return &Callback{ Identity: connector.Identity{ UserID: "0-385-28089-0", @@ -29,35 +29,34 @@ func NewCallbackConnector(logger log.Logger) connector.Connector { } var ( - _ connector.CallbackConnector = &Callback{} - - _ connector.PasswordConnector = passwordConnector{} - _ connector.RefreshConnector = passwordConnector{} + _ connector.CallbackConnector = &Callback{} + _ connector.RefreshConnector = &Callback{} + _ connector.TokenIdentityConnector = &Callback{} ) // Callback is a connector that requires no user interaction and always returns the same identity. type Callback struct { // The returned identity. Identity connector.Identity - Logger log.Logger + Logger *slog.Logger } // LoginURL returns the URL to redirect the user to login with. -func (m *Callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { +func (m *Callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, []byte, error) { u, err := url.Parse(callbackURL) if err != nil { - return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) + return "", nil, fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err) } v := u.Query() v.Set("state", state) u.RawQuery = v.Encode() - return u.String(), nil + return u.String(), nil, nil } var connectorData = []byte("foobar") // HandleCallback parses the request and returns the user's identity -func (m *Callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) { +func (m *Callback) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (connector.Identity, error) { return m.Identity, nil } @@ -66,11 +65,16 @@ func (m *Callback) Refresh(ctx context.Context, s connector.Scopes, identity con return m.Identity, nil } +func (m *Callback) TokenIdentity(ctx context.Context, subjectTokenType, subjectToken string) (connector.Identity, error) { + return m.Identity, nil +} + // CallbackConfig holds the configuration parameters for a connector which requires no interaction. type CallbackConfig struct{} // Open returns an authentication strategy which requires no user interaction. -func (c *CallbackConfig) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *CallbackConfig) Open(id string, logger *slog.Logger) (connector.Connector, error) { + logger = logger.With(slog.Group("connector", "type", "callback", "id", id)) return NewCallbackConnector(logger), nil } @@ -82,7 +86,7 @@ type PasswordConfig struct { } // Open returns an authentication strategy which prompts for a predefined username and password. -func (c *PasswordConfig) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *PasswordConfig) Open(id string, logger *slog.Logger) (connector.Connector, error) { if c.Username == "" { return nil, errors.New("no username supplied") } @@ -92,10 +96,15 @@ func (c *PasswordConfig) Open(id string, logger log.Logger) (connector.Connector return &passwordConnector{c.Username, c.Password, logger}, nil } +var ( + _ connector.PasswordConnector = passwordConnector{} + _ connector.RefreshConnector = passwordConnector{} +) + type passwordConnector struct { username string password string - logger log.Logger + logger *slog.Logger } func (p passwordConnector) Close() error { return nil } diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 237d075e83..2ae13a693b 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -2,24 +2,22 @@ package oauth import ( "context" - "crypto/tls" - "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" - "net" + "log/slog" "net/http" - "os" "strings" - "time" "golang.org/x/oauth2" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/httpclient" ) +var _ connector.CallbackConnector = (*oauthConnector)(nil) + type oauthConnector struct { clientID string clientSecret string @@ -35,7 +33,7 @@ type oauthConnector struct { emailVerifiedKey string groupsKey string httpClient *http.Client - logger log.Logger + logger *slog.Logger } type connectorData struct { @@ -62,7 +60,7 @@ type Config struct { } `json:"claimMapping"` } -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { var err error userIDKey := c.UserIDKey @@ -103,7 +101,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) userInfoURL: c.UserInfoURL, scopes: c.Scopes, redirectURI: c.RedirectURI, - logger: logger, + logger: logger.With(slog.Group("connector", "type", "oauth", "id", id)), userIDKey: userIDKey, userNameKey: userNameKey, preferredUsernameKey: preferredUsernameKey, @@ -112,7 +110,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) emailVerifiedKey: emailVerifiedKey, } - oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) + oauthConn.httpClient, err = httpclient.NewHTTPClient(c.RootCAs, c.InsecureSkipVerify) if err != nil { return nil, err } @@ -120,43 +118,9 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) return oauthConn, err } -func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { - pool, err := x509.SystemCertPool() - if err != nil { - return nil, err - } - - tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} - for _, rootCA := range rootCAs { - rootCABytes, err := os.ReadFile(rootCA) - if err != nil { - return nil, fmt.Errorf("failed to read root-ca: %v", err) - } - if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { - return nil, fmt.Errorf("no certs found in root CA file %q", rootCA) - } - } - - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tlsConfig, - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, nil -} - -func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } oauth2Config := &oauth2.Config{ @@ -167,10 +131,10 @@ func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state st Scopes: c.scopes, } - return oauth2Config.AuthCodeURL(state), nil + return oauth2Config.AuthCodeURL(state), nil, nil } -func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *oauthConnector) HandleCallback(s connector.Scopes, _ []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, errors.New(q.Get("error_description")) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 3a5ec6bf59..cdd2d3c687 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -6,15 +6,15 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/http/httptest" "net/url" "sort" "testing" - "github.com/sirupsen/logrus" + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" - jose "gopkg.in/square/go-jose.v2" "github.com/dexidp/dex/connector" ) @@ -50,7 +50,7 @@ func TestLoginURL(t *testing.T) { conn := newConnector(t, testServer.URL) - loginURL, err := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") + loginURL, _, err := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") assert.Equal(t, err, nil) expectedURL, err := url.Parse(testServer.URL + "/authorize") @@ -86,7 +86,7 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { conn := newConnector(t, testServer.URL) req := newRequestWithAuthCode(t, testServer.URL, "TestHandleCallBackForGroupsInUserInfo") - identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, nil, req) assert.Equal(t, err, nil) sort.Strings(identity.Groups) @@ -122,7 +122,7 @@ func TestHandleCallBackForGroupMapsInUserInfo(t *testing.T) { conn := newConnector(t, testServer.URL) req := newRequestWithAuthCode(t, testServer.URL, "TestHandleCallBackForGroupMapsInUserInfo") - identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, nil, req) assert.Equal(t, err, nil) sort.Strings(identity.Groups) @@ -156,7 +156,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { conn := newConnector(t, testServer.URL) req := newRequestWithAuthCode(t, testServer.URL, "TestHandleCallBackForGroupsInToken") - identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, nil, req) assert.Equal(t, err, nil) assert.Equal(t, len(identity.Groups), 1) @@ -186,7 +186,7 @@ func TestHandleCallbackForNumericUserID(t *testing.T) { conn := newConnector(t, testServer.URL) req := newRequestWithAuthCode(t, testServer.URL, "TestHandleCallbackForNumericUserID") - identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, nil, req) assert.Equal(t, err, nil) assert.Equal(t, identity.UserID, "1000") @@ -270,7 +270,7 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector { testConfig.ClaimMapping.EmailKey = "mail" testConfig.ClaimMapping.EmailVerifiedKey = "has_verified_email" - log := logrus.New() + log := slog.New(slog.DiscardHandler) conn, err := testConfig.Open("id", log) if err != nil { diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index e345dca0b2..8e1fe724c0 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -6,8 +6,10 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/url" + "regexp" "strings" "time" @@ -15,16 +17,40 @@ import ( "golang.org/x/oauth2" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + groups_pkg "github.com/dexidp/dex/pkg/groups" + "github.com/dexidp/dex/pkg/httpclient" ) +const ( + codeChallengeMethodPlain = "plain" + codeChallengeMethodS256 = "S256" +) + +func contains(arr []string, item string) bool { + for _, itemFromArray := range arr { + if itemFromArray == item { + return true + } + } + return false +} + // Config holds configuration options for OpenID Connect logins. type Config struct { - Issuer string `json:"issuer"` + Issuer string `json:"issuer"` + // Some offspec providers like Azure, Oracle IDCS have oidc discovery url + // different from issuer url which causes issuerValidation to fail + // IssuerAlias provides a way to override the Issuer url + // from the .well-known/openid-configuration issuer + IssuerAlias string `json:"issuerAlias"` ClientID string `json:"clientID"` ClientSecret string `json:"clientSecret"` RedirectURI string `json:"redirectURI"` + // The section to override options discovered automatically from + // the providers' discovery URL (.well-known/openid-configuration). + ProviderDiscoveryOverrides ProviderDiscoveryOverrides `json:"providerDiscoveryOverrides"` + // Causes client_secret to be passed as POST parameters instead of basic // auth. This is specifically "NOT RECOMMENDED" by the OAuth2 RFC, but some // providers require it. @@ -34,17 +60,32 @@ type Config struct { Scopes []string `json:"scopes"` // defaults to "profile" and "email" + // HostedDomains was an optional list of whitelisted domains when using the OIDC connector with Google. + // Only users from a whitelisted domain were allowed to log in. + // Support for this option was removed from the OIDC connector. + // Consider switching to the Google connector which supports this option. + // + // Deprecated: will be removed in future releases. + HostedDomains []string `json:"hostedDomains"` + + // Certificates for SSL validation + RootCAs []string `json:"rootCAs"` + // Override the value of email_verified to true in the returned claims InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified"` // InsecureEnableGroups enables groups claims. This is disabled by default until https://github.com/dexidp/dex/issues/1065 is resolved - InsecureEnableGroups bool `json:"insecureEnableGroups"` + InsecureEnableGroups bool `json:"insecureEnableGroups"` + AllowedGroups []string `json:"allowedGroups"` // AcrValues (Authentication Context Class Reference Values) that specifies the Authentication Context Class Values // within the Authentication Request that the Authorization Server is being requested to use for // processing requests from this Client, with the values appearing in order of preference. AcrValues []string `json:"acrValues"` + // Disable certificate verification + InsecureSkipVerify bool `json:"insecureSkipVerify"` + // GetUserInfo uses the userinfo endpoint to get additional claims for // the token. This is especially useful where upstreams return "thin" // id tokens @@ -54,8 +95,12 @@ type Config struct { UserNameKey string `json:"userNameKey"` - // PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent) - PromptType string `json:"promptType"` + // PromptType will be used for the prompt parameter (when offline_access, by default prompt=consent) + PromptType *string `json:"promptType"` + + // PKCEChallenge specifies which PKCE algorithm will be used + // If not setted it will be auto-detected the best-fit for the connector. + PKCEChallenge string `json:"pkceChallenge"` // OverrideClaimMapping will be used to override the options defined in claimMappings. // i.e. if there are 'email' and `preferred_email` claims available, by default Dex will always use the `email` claim independent of the ClaimMapping.EmailKey. @@ -72,6 +117,101 @@ type Config struct { // Configurable key which contains the groups claims GroupsKey string `json:"groups"` // defaults to "groups" } `json:"claimMapping"` + + // ClaimMutations holds all claim mutations options + ClaimMutations struct { + NewGroupFromClaims []NewGroupFromClaims `json:"newGroupFromClaims"` + FilterGroupClaims FilterGroupClaims `json:"filterGroupClaims"` + ModifyGroupNames ModifyGroupNames `json:"modifyGroupNames"` + } `json:"claimModifications"` +} + +type ProviderDiscoveryOverrides struct { + // TokenURL provides a way to user overwrite the Token URL + // from the .well-known/openid-configuration token_endpoint + TokenURL string `json:"tokenURL"` + // AuthURL provides a way to user overwrite the Auth URL + // from the .well-known/openid-configuration authorization_endpoint + AuthURL string `json:"authURL"` + // JWKSURL provides a way to user overwrite the JWKS URL + // from the .well-known/openid-configuration jwks_uri + JWKSURL string `json:"jwksURL"` +} + +func (o *ProviderDiscoveryOverrides) Empty() bool { + return o.TokenURL == "" && o.AuthURL == "" && o.JWKSURL == "" +} + +func getProvider(ctx context.Context, issuer string, overrides ProviderDiscoveryOverrides) (*oidc.Provider, error) { + provider, err := oidc.NewProvider(ctx, issuer) + if err != nil { + return nil, fmt.Errorf("failed to get provider: %v", err) + } + + if overrides.Empty() { + return provider, nil + } + + v := &struct { + Issuer string `json:"issuer"` + AuthURL string `json:"authorization_endpoint"` + TokenURL string `json:"token_endpoint"` + DeviceAuthURL string `json:"device_authorization_endpoint"` + JWKSURL string `json:"jwks_uri"` + UserInfoURL string `json:"userinfo_endpoint"` + Algorithms []string `json:"id_token_signing_alg_values_supported"` + }{} + if err := provider.Claims(v); err != nil { + return nil, fmt.Errorf("failed to extract provider discovery claims: %v", err) + } + config := oidc.ProviderConfig{ + IssuerURL: v.Issuer, + AuthURL: v.AuthURL, + TokenURL: v.TokenURL, + DeviceAuthURL: v.DeviceAuthURL, + JWKSURL: v.JWKSURL, + UserInfoURL: v.UserInfoURL, + Algorithms: v.Algorithms, + } + + if overrides.TokenURL != "" { + config.TokenURL = overrides.TokenURL + } + if overrides.AuthURL != "" { + config.AuthURL = overrides.AuthURL + } + if overrides.JWKSURL != "" { + config.JWKSURL = overrides.JWKSURL + } + return config.NewProvider(context.Background()), nil +} + +// NewGroupFromClaims creates a new group from a list of claims and appends it to the list of existing groups. +type NewGroupFromClaims struct { + // List of claim to join together + Claims []string `json:"claims"` + + // String to separate the claims + Delimiter string `json:"delimiter"` + + // Should Dex remove the Delimiter string from claim values + // This is done to keep resulting claim structure in full control of the Dex operator + ClearDelimiter bool `json:"clearDelimiter"` + + // String to place before the first claim + Prefix string `json:"prefix"` +} + +// FilterGroupClaims is a regex filter for to keep only the matching groups. +// This is useful when the groups list is too large to fit within an HTTP header. +type FilterGroupClaims struct { + GroupsFilter string `json:"groupsFilter"` +} + +// ModifyGroupNames allows to modify the group claims by adding a prefix and/or suffix to each group. +type ModifyGroupNames struct { + Prefix string `json:"prefix"` + Suffix string `json:"suffix"` } // Domains that don't support basic auth. golang.org/x/oauth2 has an internal @@ -102,15 +242,49 @@ func knownBrokenAuthHeaderProvider(issuerURL string) bool { return false } +// PKCEChallengeData is used to store info for PKCE Challenge method and verifier +// in the connectorData +type PKCEChallengeData struct { + CodeChallenge string `json:"codeChallenge"` + CodeChallengeMethod string `json:"codeChallengeMethod"` +} + +// Returns an AuthCodeOption according to the provided codeChallengeMethod +func getAuthCodeOptionForCodeChallenge(codeVerifier, codeChallengeMethod string) (oauth2.AuthCodeOption, error) { + switch codeChallengeMethod { + case codeChallengeMethodPlain: + return oauth2.VerifierOption(codeVerifier), nil + case codeChallengeMethodS256: + return oauth2.S256ChallengeOption(codeVerifier), nil + default: + return nil, fmt.Errorf("unknown challenge method (%v)", codeChallengeMethod) + } +} + // Open returns a connector which can be used to login users through an upstream // OpenID Connect provider. -func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) { - ctx, cancel := context.WithCancel(context.Background()) +func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) { + if len(c.HostedDomains) > 0 { + return nil, fmt.Errorf("support for the Hosted domains option had been deprecated and removed, consider switching to the Google connector") + } - provider, err := oidc.NewProvider(ctx, c.Issuer) + httpClient, err := httpclient.NewHTTPClient(c.RootCAs, c.InsecureSkipVerify) + if err != nil { + return nil, err + } + + bgctx, cancel := context.WithCancel(context.Background()) + ctx := context.WithValue(bgctx, oauth2.HTTPClient, httpClient) + if c.IssuerAlias != "" { + ctx = oidc.InsecureIssuerURLContext(ctx, c.IssuerAlias) + } + provider, err := getProvider(ctx, c.Issuer, c.ProviderDiscoveryOverrides) if err != nil { cancel() - return nil, fmt.Errorf("failed to get provider: %v", err) + return nil, err + } + if !c.ProviderDiscoveryOverrides.Empty() { + logger.Warn("overrides for connector are set, this can be a vulnerability when not properly configured", "connector_id", id) } endpoint := provider.Endpoint() @@ -132,8 +306,38 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e } // PromptType should be "consent" by default, if not set - if c.PromptType == "" { - c.PromptType = "consent" + promptType := "consent" + if c.PromptType != nil { + promptType = *c.PromptType + } + + var groupsFilter *regexp.Regexp + if c.ClaimMutations.FilterGroupClaims.GroupsFilter != "" { + groupsFilter, err = regexp.Compile(c.ClaimMutations.FilterGroupClaims.GroupsFilter) + if err != nil { + logger.Warn("ignoring invalid", "invalid_regex", c.ClaimMutations.FilterGroupClaims.GroupsFilter, "connector_id", id) + } + } + + // Obtain CodeChallengeMethodsSupported from the provider + var metadata struct { + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + } + if err := provider.Claims(&metadata); err != nil { + logger.Warn("failed to parse provider metadata") + } + // if PKCEChallenge method has not been setted in the config, auto-detect the best fit + if c.PKCEChallenge == "" { + if contains(metadata.CodeChallengeMethodsSupported, codeChallengeMethodS256) { + c.PKCEChallenge = codeChallengeMethodS256 + } else if contains(metadata.CodeChallengeMethodsSupported, codeChallengeMethodPlain) { + c.PKCEChallenge = codeChallengeMethodPlain + } + } else { + // if PKCEChallenge method has been setted in the config, check if it is supported + if !contains(metadata.CodeChallengeMethodsSupported, c.PKCEChallenge) { + logger.Warn("provided PKCEChallenge method not supported by the connector") + } } clientID := c.ClientID @@ -147,28 +351,37 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e Scopes: scopes, RedirectURL: c.RedirectURI, }, - verifier: provider.Verifier( + verifier: provider.VerifierContext( + ctx, // Pass our ctx with customized http.Client &oidc.Config{ClientID: clientID}, ), - logger: logger, + logger: logger.With(slog.Group("connector", "type", "oidc", "id", id)), cancel: cancel, + httpClient: httpClient, insecureSkipEmailVerified: c.InsecureSkipEmailVerified, insecureEnableGroups: c.InsecureEnableGroups, + allowedGroups: c.AllowedGroups, acrValues: c.AcrValues, getUserInfo: c.GetUserInfo, - promptType: c.PromptType, + promptType: promptType, userIDKey: c.UserIDKey, userNameKey: c.UserNameKey, overrideClaimMapping: c.OverrideClaimMapping, preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey, emailKey: c.ClaimMapping.EmailKey, groupsKey: c.ClaimMapping.GroupsKey, + newGroupFromClaims: c.ClaimMutations.NewGroupFromClaims, + groupsFilter: groupsFilter, + groupsPrefix: c.ClaimMutations.ModifyGroupNames.Prefix, + groupsSuffix: c.ClaimMutations.ModifyGroupNames.Suffix, + pkceChallenge: c.PKCEChallenge, }, nil } var ( - _ connector.CallbackConnector = (*oidcConnector)(nil) - _ connector.RefreshConnector = (*oidcConnector)(nil) + _ connector.CallbackConnector = (*oidcConnector)(nil) + _ connector.RefreshConnector = (*oidcConnector)(nil) + _ connector.TokenIdentityConnector = (*oidcConnector)(nil) ) type oidcConnector struct { @@ -177,9 +390,11 @@ type oidcConnector struct { oauth2Config *oauth2.Config verifier *oidc.IDTokenVerifier cancel context.CancelFunc - logger log.Logger + logger *slog.Logger + httpClient *http.Client insecureSkipEmailVerified bool insecureEnableGroups bool + allowedGroups []string acrValues []string getUserInfo bool promptType string @@ -189,6 +404,11 @@ type oidcConnector struct { preferredUsernameKey string emailKey string groupsKey string + newGroupFromClaims []NewGroupFromClaims + groupsFilter *regexp.Regexp + groupsPrefix string + groupsSuffix string + pkceChallenge string } func (c *oidcConnector) Close() error { @@ -196,12 +416,13 @@ func (c *oidcConnector) Close() error { return nil } -func (c *oidcConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) { +func (c *oidcConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } var opts []oauth2.AuthCodeOption + var connectorData []byte if len(c.acrValues) > 0 { acrValues := strings.Join(c.acrValues, " ") @@ -211,7 +432,25 @@ func (c *oidcConnector) LoginURL(s connector.Scopes, callbackURL, state string) if s.OfflineAccess { opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", c.promptType)) } - return c.oauth2Config.AuthCodeURL(state, opts...), nil + + if c.pkceChallenge != "" { + codeVerifier := oauth2.GenerateVerifier() + authCodeOption, err := getAuthCodeOptionForCodeChallenge(codeVerifier, c.pkceChallenge) + if err != nil { + return "", nil, fmt.Errorf("oidc: failed to get PKCE AuthCodeOption for CodeChallenge: %v", err) + } + data := PKCEChallengeData{ + CodeChallenge: codeVerifier, + CodeChallengeMethod: c.pkceChallenge, + } + connectorData, err = json.Marshal(data) + if err != nil { + return "", nil, fmt.Errorf("oidc: failed to create PKCEChallenge data: %v", err) + } + opts = append(opts, authCodeOption) + } + + return c.oauth2Config.AuthCodeURL(state, opts...), connectorData, nil } type oauth2Error struct { @@ -231,18 +470,34 @@ type caller uint const ( createCaller caller = iota refreshCaller + exchangeCaller ) -func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *oidcConnector) HandleCallback(s connector.Scopes, connData []byte, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} } - token, err := c.oauth2Config.Exchange(r.Context(), q.Get("code")) + + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient) + + var opts []oauth2.AuthCodeOption + if c.pkceChallenge != "" { + var data PKCEChallengeData + if err := json.Unmarshal(connData, &data); err != nil { + return identity, fmt.Errorf("oidc: failed to parse PKCEChallenge data: %v", err) + } + if data.CodeChallenge == "" { + return identity, fmt.Errorf("oidc: invalid PKCE CodeChallenge") + } + opts = append(opts, oauth2.VerifierOption(data.CodeChallenge)) + } + + token, err := c.oauth2Config.Exchange(ctx, q.Get("code"), opts...) if err != nil { return identity, fmt.Errorf("oidc: failed to get token: %v", err) } - return c.createIdentity(r.Context(), identity, token, createCaller) + return c.createIdentity(ctx, identity, token, createCaller) } // Refresh is used to refresh a session with the refresh token provided by the IdP @@ -253,6 +508,8 @@ func (c *oidcConnector) Refresh(ctx context.Context, s connector.Scopes, identit return identity, fmt.Errorf("oidc: failed to unmarshal connector data: %v", err) } + ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient) + t := &oauth2.Token{ RefreshToken: string(cd.RefreshToken), Expiry: time.Now().Add(-time.Hour), @@ -264,11 +521,22 @@ func (c *oidcConnector) Refresh(ctx context.Context, s connector.Scopes, identit return c.createIdentity(ctx, identity, token, refreshCaller) } +func (c *oidcConnector) TokenIdentity(ctx context.Context, subjectTokenType, subjectToken string) (connector.Identity, error) { + var identity connector.Identity + + ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient) + + token := &oauth2.Token{ + AccessToken: subjectToken, + TokenType: subjectTokenType, + } + return c.createIdentity(ctx, identity, token, exchangeCaller) +} + func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.Identity, token *oauth2.Token, caller caller) (connector.Identity, error) { var claims map[string]interface{} - rawIDToken, ok := token.Extra("id_token").(string) - if ok { + if rawIDToken, ok := token.Extra("id_token").(string); ok { idToken, err := c.verifier.Verify(ctx, rawIDToken) if err != nil { return identity, fmt.Errorf("oidc: failed to verify ID Token: %v", err) @@ -277,14 +545,36 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I if err := idToken.Claims(&claims); err != nil { return identity, fmt.Errorf("oidc: failed to decode claims: %v", err) } + } else if caller == exchangeCaller { + switch token.TokenType { + case "urn:ietf:params:oauth:token-type:id_token": + // Verify only works on ID tokens + idToken, err := c.provider.Verifier(&oidc.Config{SkipClientIDCheck: true}).Verify(ctx, token.AccessToken) + if err != nil { + return identity, fmt.Errorf("oidc: failed to verify token: %v", err) + } + if err := idToken.Claims(&claims); err != nil { + return identity, fmt.Errorf("oidc: failed to decode claims: %v", err) + } + case "urn:ietf:params:oauth:token-type:access_token": + if !c.getUserInfo { + return identity, fmt.Errorf("oidc: getUserInfo is required for access token exchange") + } + default: + return identity, fmt.Errorf("unknown token type for token exchange: %s", token.TokenType) + } } else if caller != refreshCaller { // ID tokens aren't mandatory in the reply when using a refresh_token grant return identity, errors.New("oidc: no id_token in token response") } - // We immediately want to run getUserInfo if configured before we validate the claims + // We immediately want to run getUserInfo if configured before we validate the claims. + // For token exchanges with access tokens, this is how we verify the token. if c.getUserInfo { - userInfo, err := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(token)) + userInfo, err := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token.AccessToken, + TokenType: "Bearer", // The UserInfo endpoint requires a bearer token as per RFC6750 + })) if err != nil { return identity, fmt.Errorf("oidc: error loading userinfo: %v", err) } @@ -359,12 +649,65 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I if found { for _, v := range vs { if s, ok := v.(string); ok { + if c.groupsFilter != nil && !c.groupsFilter.MatchString(s) { + continue + } groups = append(groups, s) + } else if groupMap, ok := v.(map[string]interface{}); ok { + if s, ok := groupMap["name"].(string); ok { + if c.groupsFilter != nil && !c.groupsFilter.MatchString(s) { + continue + } + groups = append(groups, s) + } } else { return identity, fmt.Errorf("malformed \"%v\" claim", groupsKey) } } } + + // Validate that the user is part of allowedGroups + if len(c.allowedGroups) > 0 { + groupMatches := groups_pkg.Filter(groups, c.allowedGroups) + + if len(groupMatches) == 0 { + // No group membership matches found, disallowing + return identity, fmt.Errorf("user not a member of allowed groups") + } + + groups = groupMatches + } + } + + // add prefix/suffix to groups + if c.groupsPrefix != "" || c.groupsSuffix != "" { + for i, group := range groups { + groups[i] = c.groupsPrefix + group + c.groupsSuffix + } + } + + for _, config := range c.newGroupFromClaims { + newGroupSegments := []string{ + config.Prefix, + } + for _, claimName := range config.Claims { + claimValue, ok := claims[claimName].(string) + if !ok { // Non string claim value are ignored, concatenating them doesn't really make any sense + continue + } + + if config.ClearDelimiter { + // Removing the delimiter string from the concatenated claim to ensure resulting claim structure + // is in full control of Dex operator + claimValue = strings.ReplaceAll(claimValue, config.Delimiter, "") + } + + newGroupSegments = append(newGroupSegments, claimValue) + } + + if len(newGroupSegments) > 1 { + groups = append(groups, strings.Join(newGroupSegments, config.Delimiter)) + } } cd := connectorData{ diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go index d94af79de8..71a30b6ed6 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -2,6 +2,7 @@ package oidc import ( "bytes" + "context" "crypto/rand" "crypto/rsa" "encoding/base64" @@ -9,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/http/httptest" "reflect" @@ -16,8 +18,8 @@ import ( "testing" "time" - "github.com/sirupsen/logrus" - "gopkg.in/square/go-jose.v2" + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/require" "github.com/dexidp/dex/connector" ) @@ -61,6 +63,11 @@ func TestHandleCallback(t *testing.T) { expectPreferredUsername string expectedEmailField string token map[string]interface{} + groupsRegex string + newGroupFromClaims []NewGroupFromClaims + groupsPrefix string + groupsSuffix string + pkceChallenge string }{ { name: "simpleCase", @@ -287,6 +294,231 @@ func TestHandleCallback(t *testing.T) { "email_verified": true, }, }, + { + name: "singularGroupResponseAsMap", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []map[string]string{{"name": "group1"}}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "multipleGroupResponseAsMap", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []map[string]string{{"name": "group1"}, {"name": "group2"}}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "newGroupFromClaims", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "gh::acme::pipeline-one", "clr_delim-acme-foobar", "keep_delim-acme-foo-bar", "bk-emailvalue"}, + expectedEmailField: "emailvalue", + newGroupFromClaims: []NewGroupFromClaims{ + { // The basic functionality, should create "gh::acme::pipeline-one". + Claims: []string{ + "organization", + "pipeline", + }, + Delimiter: "::", + Prefix: "gh", + }, + { // Non existing claims, should not generate any any new group claim. + Claims: []string{ + "non-existing1", + "non-existing2", + }, + Delimiter: "::", + Prefix: "tfe", + }, + { // In this case the delimiter character("-") should be removed removed from "claim-with-delimiter" claim to ensure the resulting + // claim structure is in full control of the Dex operator and not the person creating a new pipeline. + // Should create "clr_delim-acme-foobar" and not "tfe-acme-foo-bar". + Claims: []string{ + "organization", + "claim-with-delimiter", + }, + Delimiter: "-", + ClearDelimiter: true, + Prefix: "clr_delim", + }, + { // In this case the delimiter character("-") should be NOT removed from "claim-with-delimiter" claim. + // Should create "keep_delim-acme-foo-bar". + Claims: []string{ + "organization", + "claim-with-delimiter", + }, + Delimiter: "-", + // ClearDelimiter: false, + Prefix: "keep_delim", + }, + { // Ignore non string claims (like arrays), this should result in "bk-emailvalue". + Claims: []string{ + "non-string-claim", + "non-string-claim2", + "email", + }, + Delimiter: "-", + Prefix: "bk", + }, + }, + + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": "group1", + "organization": "acme", + "pipeline": "pipeline-one", + "email": "emailvalue", + "email_verified": true, + "claim-with-delimiter": "foo-bar", + "non-string-claim": []string{ + "element1", + "element2", + }, + "non-string-claim2": 666, + }, + }, + { + name: "prefixGroupNames", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"prefix-group1", "prefix-group2", "prefix-groupA", "prefix-groupB"}, + expectedEmailField: "emailvalue", + groupsPrefix: "prefix-", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2", "groupA", "groupB"}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "suffixGroupNames", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1-suffix", "group2-suffix", "groupA-suffix", "groupB-suffix"}, + expectedEmailField: "emailvalue", + groupsSuffix: "-suffix", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2", "groupA", "groupB"}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "preAndSuffixGroupNames", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"prefix-group1-suffix", "prefix-group2-suffix", "prefix-groupA-suffix", "prefix-groupB-suffix"}, + expectedEmailField: "emailvalue", + groupsPrefix: "prefix-", + groupsSuffix: "-suffix", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2", "groupA", "groupB"}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "filterGroupClaims", + userIDKey: "", // not configured + userNameKey: "", // not configured + groupsRegex: `^.*\d$`, + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2", "groupA", "groupB"}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "filterGroupClaimsMap", + userIDKey: "", // not configured + userNameKey: "", // not configured + groupsRegex: `^.*\d$`, + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []map[string]string{{"name": "group1"}, {"name": "group2"}, {"name": "groupA"}, {"name": "groupB"}}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "S256PKCEChallenge", + userIDKey: "", // not configured + userNameKey: "", // not configured + pkceChallenge: "S256", + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2"}, + "email": "emailvalue", + "email_verified": true, + }, + }, + { + name: "plainPKCEChallenge", + userIDKey: "", // not configured + userNameKey: "", // not configured + pkceChallenge: "plain", + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2"}, + "email": "emailvalue", + "email_verified": true, + }, + }, } for _, tc := range tests { @@ -318,10 +550,15 @@ func TestHandleCallback(t *testing.T) { InsecureEnableGroups: true, BasicAuthUnsupported: &basicAuth, OverrideClaimMapping: tc.overrideClaimMapping, + PKCEChallenge: tc.pkceChallenge, } config.ClaimMapping.PreferredUsernameKey = tc.preferredUsernameKey config.ClaimMapping.EmailKey = tc.emailKey config.ClaimMapping.GroupsKey = tc.groupsKey + config.ClaimMutations.NewGroupFromClaims = tc.newGroupFromClaims + config.ClaimMutations.FilterGroupClaims.GroupsFilter = tc.groupsRegex + config.ClaimMutations.ModifyGroupNames.Prefix = tc.groupsPrefix + config.ClaimMutations.ModifyGroupNames.Suffix = tc.groupsSuffix conn, err := newConnector(config) if err != nil { @@ -333,7 +570,11 @@ func TestHandleCallback(t *testing.T) { t.Fatal("failed to create request", err) } - identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + connectorDataStrTemplate := `{"codeChallenge":"abcdefgh123456qwertuiop89101112uvpwizABC234","codeChallengeMethod":"%s"}` + connectorDataStr := fmt.Sprintf(connectorDataStrTemplate, config.PKCEChallenge) + connectorData := []byte(connectorDataStr) + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, connectorData, req) if err != nil { t.Fatal("handle callback failed", err) } @@ -428,6 +669,171 @@ func TestRefresh(t *testing.T) { } } +func TestTokenIdentity(t *testing.T) { + tokenTypeAccess := "urn:ietf:params:oauth:token-type:access_token" + tokenTypeID := "urn:ietf:params:oauth:token-type:id_token" + long2short := map[string]string{ + tokenTypeAccess: "access_token", + tokenTypeID: "id_token", + } + + tests := []struct { + name string + subjectType string + userInfo bool + expectError bool + }{ + { + name: "id_token", + subjectType: tokenTypeID, + }, { + name: "access_token", + subjectType: tokenTypeAccess, + expectError: true, + }, { + name: "id_token with user info", + subjectType: tokenTypeID, + userInfo: true, + }, { + name: "access_token with user info", + subjectType: tokenTypeAccess, + userInfo: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + testServer, err := setupServer(map[string]any{ + "sub": "subvalue", + "name": "namevalue", + }, true) + if err != nil { + t.Fatal("failed to setup test server", err) + } + conn, err := newConnector(Config{ + Issuer: testServer.URL, + Scopes: []string{"openid", "groups"}, + GetUserInfo: tc.userInfo, + }) + if err != nil { + t.Fatal("failed to create new connector", err) + } + + res, err := http.Get(testServer.URL + "/token") + if err != nil { + t.Fatal("failed to get initial token", err) + } + defer res.Body.Close() + var tokenResponse map[string]any + err = json.NewDecoder(res.Body).Decode(&tokenResponse) + if err != nil { + t.Fatal("failed to decode initial token", err) + } + + origToken := tokenResponse[long2short[tc.subjectType]].(string) + identity, err := conn.TokenIdentity(ctx, tc.subjectType, origToken) + if err != nil { + if tc.expectError { + return + } + t.Fatal("failed to get token identity", err) + } + + // assert identity + expectEquals(t, identity.UserID, "subvalue") + expectEquals(t, identity.Username, "namevalue") + }) + } +} + +func TestPromptType(t *testing.T) { + pointer := func(s string) *string { + return &s + } + + tests := []struct { + name string + promptType *string + res string + }{ + {name: "none", promptType: pointer("none"), res: "none"}, + {name: "provided empty string", promptType: pointer(""), res: ""}, + {name: "login", promptType: pointer("login"), res: "login"}, + {name: "consent", promptType: pointer("consent"), res: "consent"}, + {name: "default value", promptType: nil, res: "consent"}, + } + + testServer, err := setupServer(nil, true) + require.NoError(t, err) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + conn, err := newConnector(Config{ + Issuer: testServer.URL, + Scopes: []string{"openid", "groups"}, + PromptType: tc.promptType, + }) + require.NoError(t, err) + + require.Equal(t, tc.res, conn.promptType) + }) + } +} + +func TestProviderOverride(t *testing.T) { + testServer, err := setupServer(map[string]any{ + "sub": "subvalue", + "name": "namevalue", + }, true) + if err != nil { + t.Fatal("failed to setup test server", err) + } + + t.Run("No override", func(t *testing.T) { + conn, err := newConnector(Config{ + Issuer: testServer.URL, + Scopes: []string{"openid", "groups"}, + }) + if err != nil { + t.Fatal("failed to create new connector", err) + } + + expAuth := fmt.Sprintf("%s/authorize", testServer.URL) + if conn.provider.Endpoint().AuthURL != expAuth { + t.Fatalf("unexpected auth URL: %s, expected: %s\n", conn.provider.Endpoint().AuthURL, expAuth) + } + + expToken := fmt.Sprintf("%s/token", testServer.URL) + if conn.provider.Endpoint().TokenURL != expToken { + t.Fatalf("unexpected token URL: %s, expected: %s\n", conn.provider.Endpoint().TokenURL, expToken) + } + }) + + t.Run("Override", func(t *testing.T) { + conn, err := newConnector(Config{ + Issuer: testServer.URL, + Scopes: []string{"openid", "groups"}, + ProviderDiscoveryOverrides: ProviderDiscoveryOverrides{TokenURL: "/test1", AuthURL: "/test2"}, + }) + if err != nil { + t.Fatal("failed to create new connector", err) + } + + expAuth := "/test2" + if conn.provider.Endpoint().AuthURL != expAuth { + t.Fatalf("unexpected auth URL: %s, expected: %s\n", conn.provider.Endpoint().AuthURL, expAuth) + } + + expToken := "/test1" + if conn.provider.Endpoint().TokenURL != expToken { + t.Fatalf("unexpected token URL: %s, expected: %s\n", conn.provider.Endpoint().TokenURL, expToken) + } + }) +} + func setupServer(tok map[string]interface{}, idTokenDesired bool) (*httptest.Server, error) { key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { @@ -523,7 +929,7 @@ func newToken(key *jose.JSONWebKey, claims map[string]interface{}) (string, erro } func newConnector(config Config) (*oidcConnector, error) { - logger := logrus.New() + logger := slog.New(slog.DiscardHandler) conn, err := config.Open("id", logger) if err != nil { return nil, fmt.Errorf("unable to open: %v", err) diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go index 81d2b35633..3d4408c585 100644 --- a/connector/openshift/openshift.go +++ b/connector/openshift/openshift.go @@ -2,22 +2,18 @@ package openshift import ( "context" - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" "io" - "net" + "log/slog" "net/http" - "os" "strings" - "time" "golang.org/x/oauth2" "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/httpclient" "github.com/dexidp/dex/storage/kubernetes/k8sapi" ) @@ -48,7 +44,7 @@ type openshiftConnector struct { clientID string clientSecret string cancel context.CancelFunc - logger log.Logger + logger *slog.Logger httpClient *http.Client oauth2Config *oauth2.Config insecureCA bool @@ -66,8 +62,13 @@ type user struct { // Open returns a connector which can be used to login users through an upstream // OpenShift OAuth2 provider. -func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) { - httpClient, err := newHTTPClient(c.InsecureCA, c.RootCA) +func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) { + var rootCAs []string + if c.RootCA != "" { + rootCAs = append(rootCAs, c.RootCA) + } + + httpClient, err := httpclient.NewHTTPClient(rootCAs, c.InsecureCA) if err != nil { return nil, fmt.Errorf("failed to create HTTP client: %w", err) } @@ -77,13 +78,17 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e // OpenWithHTTPClient returns a connector which can be used to login users through an upstream // OpenShift OAuth2 provider. It provides the ability to inject a http.Client. -func (c *Config) OpenWithHTTPClient(id string, logger log.Logger, +func (c *Config) OpenWithHTTPClient(id string, logger *slog.Logger, httpClient *http.Client, ) (conn connector.Connector, err error) { ctx, cancel := context.WithCancel(context.Background()) + defer cancel() wellKnownURL := strings.TrimSuffix(c.Issuer, "/") + wellKnownURLPath req, err := http.NewRequest(http.MethodGet, wellKnownURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create a request to OpenShift endpoint %w", err) + } openshiftConnector := openshiftConnector{ apiURL: c.Issuer, @@ -91,7 +96,7 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger, clientID: c.ClientID, clientSecret: c.ClientSecret, insecureCA: c.InsecureCA, - logger: logger, + logger: logger.With(slog.Group("connector", "type", "openshift", "id", id)), redirectURI: c.RedirectURI, rootCA: c.RootCA, groups: c.Groups, @@ -105,14 +110,12 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger, resp, err := openshiftConnector.httpClient.Do(req.WithContext(ctx)) if err != nil { - cancel() return nil, fmt.Errorf("failed to query OpenShift endpoint %w", err) } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil { - cancel() return nil, fmt.Errorf("discovery through endpoint %s failed to decode body: %w", wellKnownURL, err) } @@ -135,12 +138,12 @@ func (c *openshiftConnector) Close() error { } // LoginURL returns the URL to redirect the user to login with. -func (c *openshiftConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *openshiftConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { - return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", + return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } - return c.oauth2Config.AuthCodeURL(state), nil + return c.oauth2Config.AuthCodeURL(state), nil, nil } type oauth2Error struct { @@ -157,6 +160,7 @@ func (e *oauth2Error) Error() string { // HandleCallback parses the request and returns the user's identity func (c *openshiftConnector) HandleCallback(s connector.Scopes, + connData []byte, r *http.Request, ) (identity connector.Identity, err error) { q := r.URL.Query() @@ -262,36 +266,3 @@ func validateAllowedGroups(userGroups, allowedGroups []string) bool { return len(matchingGroups) != 0 } - -// newHTTPClient returns a new HTTP client -func newHTTPClient(insecureCA bool, rootCA string) (*http.Client, error) { - tlsConfig := tls.Config{} - if insecureCA { - tlsConfig = tls.Config{InsecureSkipVerify: true} - } else if rootCA != "" { - tlsConfig = tls.Config{RootCAs: x509.NewCertPool()} - rootCABytes, err := os.ReadFile(rootCA) - if err != nil { - return nil, fmt.Errorf("failed to read root-ca: %w", err) - } - if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { - return nil, fmt.Errorf("no certs found in root CA file %q", rootCA) - } - } - - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tlsConfig, - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, nil -} diff --git a/connector/openshift/openshift_test.go b/connector/openshift/openshift_test.go index 6280b831de..bdddfc83be 100644 --- a/connector/openshift/openshift_test.go +++ b/connector/openshift/openshift_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "net/http" "net/http/httptest" "net/url" @@ -11,10 +12,10 @@ import ( "testing" "time" - "github.com/sirupsen/logrus" "golang.org/x/oauth2" "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/pkg/httpclient" "github.com/dexidp/dex/storage/kubernetes/k8sapi" ) @@ -36,7 +37,7 @@ func TestOpen(t *testing.T) { InsecureCA: true, } - logger := logrus.New() + logger := slog.New(slog.DiscardHandler) oconfig, err := c.Open("id", logger) @@ -70,7 +71,7 @@ func TestGetUser(t *testing.T) { _, err = http.NewRequest("GET", hostURL.String(), nil) expectNil(t, err) - h, err := newHTTPClient(true, "") + h, err := httpclient.NewHTTPClient(nil, true) expectNil(t, err) @@ -128,7 +129,7 @@ func TestVerifyGroup(t *testing.T) { _, err = http.NewRequest("GET", hostURL.String(), nil) expectNil(t, err) - h, err := newHTTPClient(true, "") + h, err := httpclient.NewHTTPClient(nil, true) expectNil(t, err) @@ -164,7 +165,7 @@ func TestCallbackIdentity(t *testing.T) { req, err := http.NewRequest("GET", hostURL.String(), nil) expectNil(t, err) - h, err := newHTTPClient(true, "") + h, err := httpclient.NewHTTPClient(nil, true) expectNil(t, err) @@ -174,7 +175,7 @@ func TestCallbackIdentity(t *testing.T) { TokenURL: fmt.Sprintf("%s/oauth/token", s.URL), }, }} - identity, err := oc.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := oc.HandleCallback(connector.Scopes{Groups: true}, nil, req) expectNil(t, err) expectEquals(t, identity.UserID, "12345") @@ -198,7 +199,7 @@ func TestRefreshIdentity(t *testing.T) { }) defer s.Close() - h, err := newHTTPClient(true, "") + h, err := httpclient.NewHTTPClient(nil, true) expectNil(t, err) oc := openshiftConnector{apiURL: s.URL, httpClient: h, oauth2Config: &oauth2.Config{ @@ -237,7 +238,7 @@ func TestRefreshIdentityFailure(t *testing.T) { }) defer s.Close() - h, err := newHTTPClient(true, "") + h, err := httpclient.NewHTTPClient(nil, true) expectNil(t, err) oc := openshiftConnector{apiURL: s.URL, httpClient: h, oauth2Config: &oauth2.Config{ diff --git a/connector/saml/saml.go b/connector/saml/saml.go index 908ec703c9..8ef434b62a 100644 --- a/connector/saml/saml.go +++ b/connector/saml/saml.go @@ -3,11 +3,14 @@ package saml import ( "bytes" + "context" "crypto/x509" "encoding/base64" + "encoding/json" "encoding/pem" "encoding/xml" "fmt" + "log/slog" "os" "strings" "sync" @@ -21,10 +24,8 @@ import ( "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" ) -// nolint const ( bindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" bindingPOST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" @@ -120,11 +121,12 @@ func (c certStore) Certificates() (roots []*x509.Certificate, err error) { // Open validates the config and returns a connector. It does not actually // validate connectivity with the provider. -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { + logger = logger.With(slog.Group("connector", "type", "saml", "id", id)) return c.openConnector(logger) } -func (c *Config) openConnector(logger log.Logger) (*provider, error) { +func (c *Config) openConnector(logger *slog.Logger) (*provider, error) { requiredFields := []struct { name, val string }{ @@ -230,6 +232,11 @@ func (c *Config) openConnector(logger log.Logger) (*provider, error) { return p, nil } +var ( + _ connector.SAMLConnector = (*provider)(nil) + _ connector.RefreshConnector = (*provider)(nil) +) + type provider struct { entityIssuer string ssoIssuer string @@ -252,7 +259,37 @@ type provider struct { nameIDPolicyFormat string - logger log.Logger + logger *slog.Logger +} + +// cachedIdentity stores the identity from SAML assertion for refresh token support. +// Since SAML has no native refresh mechanism, we cache the identity obtained during +// the initial authentication and return it on subsequent refresh requests. +type cachedIdentity struct { + UserID string `json:"userId"` + Username string `json:"username"` + PreferredUsername string `json:"preferredUsername"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + Groups []string `json:"groups,omitempty"` +} + +// marshalCachedIdentity serializes the identity into ConnectorData for refresh token support. +func marshalCachedIdentity(ident connector.Identity) (connector.Identity, error) { + ci := cachedIdentity{ + UserID: ident.UserID, + Username: ident.Username, + PreferredUsername: ident.PreferredUsername, + Email: ident.Email, + EmailVerified: ident.EmailVerified, + Groups: ident.Groups, + } + connectorData, err := json.Marshal(ci) + if err != nil { + return ident, fmt.Errorf("saml: failed to marshal cached identity: %v", err) + } + ident.ConnectorData = connectorData + return ident, nil } func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) { @@ -292,7 +329,6 @@ func (p *provider) POSTData(s connector.Scopes, id string) (action, value string // * Verify signature on XML document (or verify sig on assertion elements). // * Verify various parts of the Assertion element. Conditions, audience, etc. // * Map the Assertion's attribute elements to user info. -// func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo string) (ident connector.Identity, err error) { rawResp, err := base64.StdEncoding.DecodeString(samlResponse) if err != nil { @@ -390,7 +426,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str // Log the actual attributes we got back from the server. This helps debug // configuration errors on the server side, where the SAML server doesn't // send us the correct attributes. - p.logger.Infof("parsed and verified saml response attributes %s", attributes) + p.logger.Info("parsed and verified saml response attributes", "attributes", attributes) // Grab the email. if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" { @@ -406,7 +442,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str if len(p.allowedGroups) == 0 && (!s.Groups || p.groupsAttr == "") { // Groups not requested or not configured. We're done. - return ident, nil + return marshalCachedIdentity(ident) } if len(p.allowedGroups) > 0 && (!s.Groups || p.groupsAttr == "") { @@ -432,7 +468,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str if len(p.allowedGroups) == 0 { // No allowed groups set, just return the ident - return ident, nil + return marshalCachedIdentity(ident) } // Look for membership in one of the allowed groups @@ -448,6 +484,35 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str } // Otherwise, we're good + return marshalCachedIdentity(ident) +} + +// Refresh implements connector.RefreshConnector. +// Since SAML has no native refresh mechanism, this method returns the cached +// identity from the initial SAML assertion stored in ConnectorData. +func (p *provider) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) { + if len(ident.ConnectorData) == 0 { + return ident, fmt.Errorf("saml: no connector data available for refresh") + } + + var ci cachedIdentity + if err := json.Unmarshal(ident.ConnectorData, &ci); err != nil { + return ident, fmt.Errorf("saml: failed to unmarshal cached identity: %v", err) + } + + ident.UserID = ci.UserID + ident.Username = ci.Username + ident.PreferredUsername = ci.PreferredUsername + ident.Email = ci.Email + ident.EmailVerified = ci.EmailVerified + + // Only populate groups if the client requested the groups scope. + if s.Groups { + ident.Groups = ci.Groups + } else { + ident.Groups = nil + } + return ident, nil } @@ -468,7 +533,7 @@ func (p *provider) validateStatus(status *status) error { if statusMessage != nil && statusMessage.Value != "" { errorMessage += " -> " + statusMessage.Value } - return fmt.Errorf(errorMessage) + return errors.New(errorMessage) } return nil } @@ -531,7 +596,7 @@ func (p *provider) validateSubject(subject *subject, inResponseTo string) error return fmt.Errorf("failed to validate subject confirmation: %v", errs) } -// validationConditions ensures that dex is the intended audience +// validateConditions ensures that dex is the intended audience // for the request, and not another service provider. // // See: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf @@ -598,6 +663,9 @@ func verifyResponseSig(validator *dsig.ValidationContext, data []byte) (signed [ } response := doc.Root() + if response == nil { + return nil, false, fmt.Errorf("parse document: empty root") + } transformedResponse, err := validator.Validate(response) if err == nil { // Root element is verified, return it. @@ -610,7 +678,7 @@ func verifyResponseSig(validator *dsig.ValidationContext, data []byte) (signed [ // // TODO: Only select from child elements of the root. assertion, err := etreeutils.NSSelectOne(response, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion") - if err != nil { + if err != nil || assertion == nil { return nil, false, fmt.Errorf("response does not contain an Assertion element") } transformedAssertion, err := validator.Validate(assertion) diff --git a/connector/saml/saml_test.go b/connector/saml/saml_test.go index 95d513ed19..3eba5cf878 100644 --- a/connector/saml/saml_test.go +++ b/connector/saml/saml_test.go @@ -1,10 +1,13 @@ package saml import ( + "context" "crypto/x509" "encoding/base64" + "encoding/json" "encoding/pem" "errors" + "log/slog" "os" "sort" "testing" @@ -12,7 +15,6 @@ import ( "github.com/kylelemons/godebug/pretty" dsig "github.com/russellhaering/goxmldsig" - "github.com/sirupsen/logrus" "github.com/dexidp/dex/connector" ) @@ -24,19 +26,18 @@ import ( // To add a new test, define a new, unsigned SAML 2.0 response that exercises some // case, then sign it using the "testdata/gen.sh" script. // -// cp testdata/good-resp.tmpl testdata/( testname ).tmpl -// vim ( testname ).tmpl # Modify your template for your test case. -// vim testdata/gen.sh # Add a xmlsec1 command to the generation script. -// ./testdata/gen.sh # Sign your template. +// cp testdata/good-resp.tmpl testdata/( testname ).tmpl +// vim ( testname ).tmpl # Modify your template for your test case. +// vim testdata/gen.sh # Add a xmlsec1 command to the generation script. +// ./testdata/gen.sh # Sign your template. // // To install xmlsec1 on Fedora run: // -// sudo dnf install xmlsec1 xmlsec1-openssl +// sudo dnf install xmlsec1 xmlsec1-openssl // // On mac: // -// brew install Libxmlsec1 -// +// brew install Libxmlsec1 type responseTest struct { // CA file and XML file of the response. caFile string @@ -421,7 +422,7 @@ func (r responseTest) run(t *testing.T) { t.Fatalf("parse test time: %v", err) } - conn, err := c.openConnector(logrus.New()) + conn, err := c.openConnector(slog.New(slog.DiscardHandler)) if err != nil { t.Fatal(err) } @@ -449,13 +450,31 @@ func (r responseTest) run(t *testing.T) { } sort.Strings(ident.Groups) sort.Strings(r.wantIdent.Groups) + + // Verify ConnectorData contains valid cached identity, then clear it + // for the main identity comparison (ConnectorData is an implementation + // detail of refresh token support). + if len(ident.ConnectorData) > 0 { + var ci cachedIdentity + if err := json.Unmarshal(ident.ConnectorData, &ci); err != nil { + t.Fatalf("failed to unmarshal ConnectorData: %v", err) + } + if ci.UserID != ident.UserID { + t.Errorf("cached identity UserID mismatch: got %q, want %q", ci.UserID, ident.UserID) + } + if ci.Email != ident.Email { + t.Errorf("cached identity Email mismatch: got %q, want %q", ci.Email, ident.Email) + } + } + ident.ConnectorData = nil + if diff := pretty.Compare(ident, r.wantIdent); diff != "" { t.Error(diff) } } func TestConfigCAData(t *testing.T) { - logger := logrus.New() + logger := slog.New(slog.DiscardHandler) validPEM, err := os.ReadFile("testdata/ca.crt") if err != nil { t.Fatal(err) @@ -590,3 +609,310 @@ func TestVerifySignedMessageAndSignedAssertion(t *testing.T) { func TestVerifyUnsignedMessageAndUnsignedAssertion(t *testing.T) { runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp.xml", false) } + +func TestSAMLRefresh(t *testing.T) { + // Create a provider using the same pattern as existing tests. + c := Config{ + CA: "testdata/ca.crt", + UsernameAttr: "Name", + EmailAttr: "email", + GroupsAttr: "groups", + RedirectURI: "http://127.0.0.1:5556/dex/callback", + SSOURL: "http://foo.bar/", + } + + conn, err := c.openConnector(slog.New(slog.DiscardHandler)) + if err != nil { + t.Fatal(err) + } + + t.Run("SuccessfulRefresh", func(t *testing.T) { + ci := cachedIdentity{ + UserID: "test-user-id", + Username: "testuser", + PreferredUsername: "testuser", + Email: "test@example.com", + EmailVerified: true, + Groups: []string{"group1", "group2"}, + } + connectorData, err := json.Marshal(ci) + if err != nil { + t.Fatal(err) + } + + ident := connector.Identity{ + UserID: "old-id", + Username: "old-name", + ConnectorData: connectorData, + } + + refreshed, err := conn.Refresh(context.Background(), connector.Scopes{Groups: true}, ident) + if err != nil { + t.Fatalf("Refresh failed: %v", err) + } + + if refreshed.UserID != "test-user-id" { + t.Errorf("expected UserID %q, got %q", "test-user-id", refreshed.UserID) + } + if refreshed.Username != "testuser" { + t.Errorf("expected Username %q, got %q", "testuser", refreshed.Username) + } + if refreshed.PreferredUsername != "testuser" { + t.Errorf("expected PreferredUsername %q, got %q", "testuser", refreshed.PreferredUsername) + } + if refreshed.Email != "test@example.com" { + t.Errorf("expected Email %q, got %q", "test@example.com", refreshed.Email) + } + if !refreshed.EmailVerified { + t.Error("expected EmailVerified to be true") + } + if len(refreshed.Groups) != 2 || refreshed.Groups[0] != "group1" || refreshed.Groups[1] != "group2" { + t.Errorf("expected groups [group1, group2], got %v", refreshed.Groups) + } + // ConnectorData should be preserved through refresh + if len(refreshed.ConnectorData) == 0 { + t.Error("expected ConnectorData to be preserved") + } + }) + + t.Run("RefreshPreservesConnectorData", func(t *testing.T) { + ci := cachedIdentity{ + UserID: "user-123", + Username: "alice", + Email: "alice@example.com", + EmailVerified: true, + } + connectorData, err := json.Marshal(ci) + if err != nil { + t.Fatal(err) + } + + ident := connector.Identity{ + UserID: "old-id", + ConnectorData: connectorData, + } + + refreshed, err := conn.Refresh(context.Background(), connector.Scopes{}, ident) + if err != nil { + t.Fatalf("Refresh failed: %v", err) + } + + // Verify the refreshed identity can be refreshed again (round-trip) + var roundTrip cachedIdentity + if err := json.Unmarshal(refreshed.ConnectorData, &roundTrip); err != nil { + t.Fatalf("failed to unmarshal ConnectorData after refresh: %v", err) + } + if roundTrip.UserID != "user-123" { + t.Errorf("round-trip UserID mismatch: got %q, want %q", roundTrip.UserID, "user-123") + } + }) + + t.Run("EmptyConnectorData", func(t *testing.T) { + ident := connector.Identity{ + UserID: "test-id", + ConnectorData: nil, + } + _, err := conn.Refresh(context.Background(), connector.Scopes{}, ident) + if err == nil { + t.Error("expected error for empty ConnectorData") + } + }) + + t.Run("InvalidJSON", func(t *testing.T) { + ident := connector.Identity{ + UserID: "test-id", + ConnectorData: []byte("not-json"), + } + _, err := conn.Refresh(context.Background(), connector.Scopes{}, ident) + if err == nil { + t.Error("expected error for invalid JSON") + } + }) + + t.Run("HandlePOSTThenRefresh", func(t *testing.T) { + // Full integration: HandlePOST → get ConnectorData → Refresh → verify identity + now, err := time.Parse(timeFormat, "2017-04-04T04:34:59.330Z") + if err != nil { + t.Fatal(err) + } + conn.now = func() time.Time { return now } + + resp, err := os.ReadFile("testdata/good-resp.xml") + if err != nil { + t.Fatal(err) + } + samlResp := base64.StdEncoding.EncodeToString(resp) + + scopes := connector.Scopes{ + OfflineAccess: true, + Groups: true, + } + ident, err := conn.HandlePOST(scopes, samlResp, "6zmm5mguyebwvajyf2sdwwcw6m") + if err != nil { + t.Fatalf("HandlePOST failed: %v", err) + } + + if len(ident.ConnectorData) == 0 { + t.Fatal("expected ConnectorData to be set after HandlePOST") + } + + // Now refresh using the ConnectorData from HandlePOST + refreshed, err := conn.Refresh(context.Background(), scopes, ident) + if err != nil { + t.Fatalf("Refresh failed: %v", err) + } + + if refreshed.UserID != ident.UserID { + t.Errorf("UserID mismatch: got %q, want %q", refreshed.UserID, ident.UserID) + } + if refreshed.Username != ident.Username { + t.Errorf("Username mismatch: got %q, want %q", refreshed.Username, ident.Username) + } + if refreshed.Email != ident.Email { + t.Errorf("Email mismatch: got %q, want %q", refreshed.Email, ident.Email) + } + if refreshed.EmailVerified != ident.EmailVerified { + t.Errorf("EmailVerified mismatch: got %v, want %v", refreshed.EmailVerified, ident.EmailVerified) + } + sort.Strings(refreshed.Groups) + sort.Strings(ident.Groups) + if len(refreshed.Groups) != len(ident.Groups) { + t.Errorf("Groups length mismatch: got %d, want %d", len(refreshed.Groups), len(ident.Groups)) + } + for i := range ident.Groups { + if i < len(refreshed.Groups) && refreshed.Groups[i] != ident.Groups[i] { + t.Errorf("Groups[%d] mismatch: got %q, want %q", i, refreshed.Groups[i], ident.Groups[i]) + } + } + }) + + t.Run("HandlePOSTThenDoubleRefresh", func(t *testing.T) { + // Verify that refresh tokens can be chained: HandlePOST → Refresh → Refresh + now, err := time.Parse(timeFormat, "2017-04-04T04:34:59.330Z") + if err != nil { + t.Fatal(err) + } + conn.now = func() time.Time { return now } + + resp, err := os.ReadFile("testdata/good-resp.xml") + if err != nil { + t.Fatal(err) + } + samlResp := base64.StdEncoding.EncodeToString(resp) + + scopes := connector.Scopes{OfflineAccess: true, Groups: true} + ident, err := conn.HandlePOST(scopes, samlResp, "6zmm5mguyebwvajyf2sdwwcw6m") + if err != nil { + t.Fatalf("HandlePOST failed: %v", err) + } + + // First refresh + refreshed1, err := conn.Refresh(context.Background(), scopes, ident) + if err != nil { + t.Fatalf("first Refresh failed: %v", err) + } + if len(refreshed1.ConnectorData) == 0 { + t.Fatal("expected ConnectorData after first refresh") + } + + // Second refresh using output of first refresh + refreshed2, err := conn.Refresh(context.Background(), scopes, refreshed1) + if err != nil { + t.Fatalf("second Refresh failed: %v", err) + } + + // All fields should match original + if refreshed2.UserID != ident.UserID { + t.Errorf("UserID mismatch after double refresh: got %q, want %q", refreshed2.UserID, ident.UserID) + } + if refreshed2.Email != ident.Email { + t.Errorf("Email mismatch after double refresh: got %q, want %q", refreshed2.Email, ident.Email) + } + if refreshed2.Username != ident.Username { + t.Errorf("Username mismatch after double refresh: got %q, want %q", refreshed2.Username, ident.Username) + } + }) + + t.Run("HandlePOSTWithAssertionSignedThenRefresh", func(t *testing.T) { + // Test with assertion-signed.xml (signature on assertion, not response) + now, err := time.Parse(timeFormat, "2017-04-04T04:34:59.330Z") + if err != nil { + t.Fatal(err) + } + conn.now = func() time.Time { return now } + + resp, err := os.ReadFile("testdata/assertion-signed.xml") + if err != nil { + t.Fatal(err) + } + samlResp := base64.StdEncoding.EncodeToString(resp) + + scopes := connector.Scopes{OfflineAccess: true, Groups: true} + ident, err := conn.HandlePOST(scopes, samlResp, "6zmm5mguyebwvajyf2sdwwcw6m") + if err != nil { + t.Fatalf("HandlePOST with assertion-signed failed: %v", err) + } + + if len(ident.ConnectorData) == 0 { + t.Fatal("expected ConnectorData after HandlePOST with assertion-signed") + } + + refreshed, err := conn.Refresh(context.Background(), scopes, ident) + if err != nil { + t.Fatalf("Refresh after assertion-signed HandlePOST failed: %v", err) + } + + if refreshed.Email != ident.Email { + t.Errorf("Email mismatch: got %q, want %q", refreshed.Email, ident.Email) + } + if refreshed.Username != ident.Username { + t.Errorf("Username mismatch: got %q, want %q", refreshed.Username, ident.Username) + } + }) + + t.Run("HandlePOSTRefreshWithoutGroupsScope", func(t *testing.T) { + // Verify that groups are NOT returned when groups scope is not requested during refresh + now, err := time.Parse(timeFormat, "2017-04-04T04:34:59.330Z") + if err != nil { + t.Fatal(err) + } + conn.now = func() time.Time { return now } + + resp, err := os.ReadFile("testdata/good-resp.xml") + if err != nil { + t.Fatal(err) + } + samlResp := base64.StdEncoding.EncodeToString(resp) + + // Initial auth WITH groups + scopesWithGroups := connector.Scopes{OfflineAccess: true, Groups: true} + ident, err := conn.HandlePOST(scopesWithGroups, samlResp, "6zmm5mguyebwvajyf2sdwwcw6m") + if err != nil { + t.Fatalf("HandlePOST failed: %v", err) + } + if len(ident.Groups) == 0 { + t.Fatal("expected groups in initial identity") + } + + // Refresh WITHOUT groups scope + scopesNoGroups := connector.Scopes{OfflineAccess: true, Groups: false} + refreshed, err := conn.Refresh(context.Background(), scopesNoGroups, ident) + if err != nil { + t.Fatalf("Refresh failed: %v", err) + } + + if len(refreshed.Groups) != 0 { + t.Errorf("expected no groups when groups scope not requested, got %v", refreshed.Groups) + } + + // Refresh WITH groups scope — groups should be back + refreshedWithGroups, err := conn.Refresh(context.Background(), scopesWithGroups, ident) + if err != nil { + t.Fatalf("Refresh with groups failed: %v", err) + } + + if len(refreshedWithGroups.Groups) == 0 { + t.Error("expected groups when groups scope is requested") + } + }) +} diff --git a/docker-compose.override.yaml.dist b/docker-compose.override.yaml.dist index b9eefac533..30591add0f 100644 --- a/docker-compose.override.yaml.dist +++ b/docker-compose.override.yaml.dist @@ -5,6 +5,10 @@ services: ports: - "127.0.0.1:3306:3306" + mysql8: + ports: + - "127.0.0.1:3307:3306" + postgres: ports: - "127.0.0.1:5432:5432" diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 46dfd84c4d..933ff80164 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -11,8 +11,8 @@ services: LDAP_TLS: "true" LDAP_TLS_VERIFY_CLIENT: try ports: - - 389:389 - - 636:636 + - 3890:389 + - 6360:636 volumes: - ./connector/ldap/testdata/certs:/container/service/slapd/assets/certs - ./connector/ldap/testdata/schema.ldif:/container/service/slapd/assets/config/bootstrap/ldif/99-schema.ldif diff --git a/docker-compose.yaml b/docker-compose.yaml index eee32f93e9..6c5a052a70 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,6 +17,15 @@ services: MYSQL_PASSWORD: mysql MYSQL_ROOT_PASSWORD: root + mysql8: + image: mysql:8.0 + command: --default-authentication-plugin=mysql_native_password + environment: + MYSQL_DATABASE: dex + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + MYSQL_ROOT_PASSWORD: root + postgres: image: postgres:10.15 environment: @@ -45,3 +54,23 @@ services: volumes: - ./connector/ldap/testdata/certs:/container/service/slapd/assets/certs - ./connector/ldap/testdata/schema.ldif:/container/service/slapd/assets/config/bootstrap/ldif/99-schema.ldif + + vault: + image: hashicorp/vault:1.21 + environment: + VAULT_DEV_ROOT_TOKEN_ID: root-token + VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200" + cap_add: + - IPC_LOCK + ports: + - 8200:8200 + + openbao: + image: quay.io/openbao/openbao:2.5 + environment: + BAO_DEV_ROOT_TOKEN_ID: root-token + BAO_DEV_LISTEN_ADDRESS: "0.0.0.0:8200" + cap_add: + - IPC_LOCK + ports: + - 8210:8200 diff --git a/docs/enhancements/auth-sessions-2026-02-18.md b/docs/enhancements/auth-sessions-2026-02-18.md new file mode 100644 index 0000000000..28f5208ccb --- /dev/null +++ b/docs/enhancements/auth-sessions-2026-02-18.md @@ -0,0 +1,1505 @@ +# Dex Enhancement Proposal (DEP 4560) - 2026-02-18 - Auth Sessions + +## Table of Contents + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals/Pain](#goalspain) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) + - [User Experience](#user-experience) + - [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints) + - [Risks and Mitigations](#risks-and-mitigations) + - [Alternatives](#alternatives) +- [Future Improvements](#future-improvements) + +## Summary + +This DEP introduces **auth sessions** - a persistent authentication state that enables Dex to track logged-in users across browser sessions. Currently, Dex relies entirely on refresh tokens for session management, which prevents proper implementation of OIDC conformance features like `prompt=none`, `prompt=login`, `id_token_hint`, SSO across clients, and proper logout. User Sessions will be stored server-side with a browser cookie reference, enabling these features while maintaining Dex's simplicity and compatibility with all storage backends (SQL, etcd, Kubernetes CRDs). + +## Context + +- [OIDC Core 1.0 - Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) - `prompt` parameter specification +- [OIDC Core 1.0 - ID Token Hint](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) - `id_token_hint` specification +- [OIDC Session Management 1.0](https://openid.net/specs/openid-connect-session-1_0.html) - Session management specification +- [OIDC RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) - Logout specification +- [OIDC Front-Channel Logout 1.0](https://openid.net/specs/openid-connect-frontchannel-1_0.html) - Front-channel logout +- [Keycloak Sessions](https://www.keycloak.org/docs/latest/server_admin/#_sessions) - Reference implementation +- [Ory Hydra Login & Consent Flow](https://www.ory.sh/docs/hydra/concepts/login) - Reference implementation + +Current limitations: +- No support for `prompt=none` (silent authentication) +- No support for `prompt=login` (force re-authentication) +- No support for `max_age` parameter +- No support for `id_token_hint` validation +- No SSO between clients (each client requires separate login) +- No proper logout (only refresh token revocation) +- No consent persistence (user must approve every time if not skipped globally) +- No 2FA enrollment storage +- No "Remember Me" functionality + +## Motivation + +### Goals/Pain + +1. **OIDC Conformance** - Enable proper `prompt=none`, `prompt=login`, `max_age`, and `id_token_hint` support +2. **SSO (Single Sign-On)** - Allow users to authenticate once and access multiple clients without re-login +3. **Remember Me** - Allow users to choose persistent vs session-based authentication +4. **Consent Persistence** - Store user consent decisions per client/scope combination within session +5. **Proper Logout** - Enable session termination with optional front-channel logout +6. **Foundation for 2FA** - Enable future TOTP/WebAuthn enrollment storage + +### Non-Goals + +- **2FA Implementation** - This DEP only provides storage foundation; 2FA flow is a separate DEP +- **Back-Channel Logout** - Server-to-server logout notifications are out of scope +- **Session Clustering/Replication** - Storage backends handle this +- **Admin Session Management UI** - API only, no admin UI +- **Per-connector Session Policies** - Single global session policy initially +- **Identity Refresh During Session** - Deferred to future DEP; initially identity is refreshed only at session termination (like Keycloak) +- **Upstream Connector Logout** - Terminating sessions at upstream IDPs is deferred + +## Proposal + +### User Experience + +#### Configuration + +Sessions are controlled by a feature flag and configuration: + +```yaml +# Feature flag (environment variable) +# DEX_SESSIONS_ENABLED=true + +# config.yaml +sessions: + # Session cookie name (default: "dex_session") + # Other cookie settings (Secure, HttpOnly, SameSite=Lax) are not configurable + # and are set to secure defaults automatically + cookieName: "dex_session" + + # Session lifetime settings (matches refresh token expiry naming) + absoluteLifetime: "24h" # Maximum session lifetime, default: 24h + validIfNotUsedFor: "1h" # Session expires if not used, default: 1h + + # Default SSO sharing policy for clients without explicit ssoSharedWith config + # Options: + # "all" - clients without ssoSharedWith share sessions with all other clients (Keycloak-like) + # "none" - clients without ssoSharedWith don't share sessions (default) + ssoSharedWithDefault: "none" + + # Whether "Remember Me" checkbox is checked by default in login/approval forms + # When true: checkbox is pre-checked, user can uncheck + # When false: checkbox is unchecked, user must check to persist session (default) + rememberMeCheckedByDefault: false +``` + +**ssoSharedWithDefault** controls the default SSO behavior: +- `"none"` (default): Clients without explicit `ssoSharedWith` config don't participate in SSO +- `"all"`: Clients without explicit `ssoSharedWith` config share sessions with all other clients (realm-wide SSO like Keycloak) + +Clients with explicit `ssoSharedWith` configuration always use their configured value. + +**Note**: The `ssoSharedWith` option is separate from the existing `trustedPeers` option. `trustedPeers` controls which clients can issue tokens on behalf of this client (existing behavior), while `ssoSharedWith` controls which clients can reuse this client's authentication session (new behavior). These can be configured independently based on different security requirements. + +**rememberMeCheckedByDefault** controls the initial checkbox state in templates. +This value is passed to templates as `.RememberMeChecked` boolean. + +**SSO via ssoSharedWith**: SSO between clients is controlled by the new `ssoSharedWith` configuration on clients. The `ssoSharedWith` setting defines **which clients can USE this client's session**, not which clients this client can use. + +If client B is listed in client A's `ssoSharedWith`: +1. If user logged in via client A, client B can reuse that session + +This is intentionally separate from `trustedPeers` (which controls token issuance on behalf of another client). Organizations may want different policies for session sharing vs token delegation: +- **ssoSharedWith**: "Can this client's login be reused by another client?" +- **trustedPeers**: "Can another client issue tokens claiming to be this client?" + +**Wildcard Support**: `ssoSharedWith: ["*"]` enables SSO with all clients. This is similar to Keycloak's default behavior where all clients in a realm share sessions. + +**SSO Direction**: SSO sharing is **unidirectional**. Client A sharing with client B does NOT mean client B shares with client A. + +```yaml +staticClients: + # Public app - allows any client to reuse its sessions + - id: public-app + name: Public App + ssoSharedWith: ["*"] + # trustedPeers can be configured separately for token delegation + # ... + + # Admin app - only specific apps can reuse its sessions + - id: admin-app + name: Admin App + ssoSharedWith: ["monitoring-app"] # Only monitoring can SSO from admin sessions + # ... + + # Secret internal service - NO other clients can reuse its sessions + - id: secret-service + name: Secret Service + ssoSharedWith: [] # Empty = no SSO allowed from this client's sessions + # But this client CAN use sessions from other clients that share with it! + # ... + + # Monitoring app - can SSO from admin-app (because admin-app shares with it) + - id: monitoring-app + name: Monitoring App + ssoSharedWith: ["admin-app"] # Bidirectional sharing with admin-app + # ... +``` + +**Example Scenarios:** + +| User logged in via | Accessing | SSO works? | Why | +|-------------------|-----------|------------|-----| +| public-app | admin-app | ✅ Yes | public-app has `ssoSharedWith: ["*"]` | +| admin-app | public-app | ❌ No | admin-app only shares with monitoring-app | +| admin-app | monitoring-app | ✅ Yes | admin-app shares with monitoring-app | +| secret-service | any client | ❌ No | secret-service has `ssoSharedWith: []` | +| public-app | secret-service | ✅ Yes | public-app has `ssoSharedWith: ["*"]` | + +**Key Insight**: A "secret" client that doesn't want others to SSO into it simply doesn't list them in `ssoSharedWith`. But it can still BENEFIT from SSO by being listed in OTHER clients' `ssoSharedWith`. + +**Comparison with Keycloak**: In Keycloak, SSO is realm-wide by default - all clients in a realm share sessions. Dex's approach is more granular: SSO is opt-in per client via `ssoSharedWith`. Use `["*"]` to achieve Keycloak-like behavior. + +**Comparison with trustedPeers**: The `trustedPeers` option continues to control cross-client token issuance (e.g., client B issuing tokens for client A). This is a separate security concern from session sharing. Organizations can configure these independently: +- High SSO sharing, restricted token delegation +- Restricted SSO sharing, high token delegation +- Or any combination based on their security model + +**Cookie Security**: The session cookie is always set with secure defaults: +- `HttpOnly: true` - Not accessible via JavaScript +- `Secure: (issuerURL.Scheme == "https")` - Only sent over HTTPS; for `http` (commonly used on localhost in dev) this is disabled +- `SameSite: Lax` - CSRF protection +- `Path: ` - Derived from issuer URL (e.g., `/dex` for `https://example.com/dex`) + +These settings are not configurable to prevent security misconfigurations. + +#### Authentication Flow with Sessions + +``` +┌─────────┐ ┌─────────┐ ┌───────────┐ ┌───────────┐ +│ Browser │ │ Dex │ │ Storage │ │ Connector │ +└────â”Ŧ────┘ └────â”Ŧ────┘ └─────â”Ŧ─────┘ └─────â”Ŧ─────┘ + │ │ │ │ + │ GET /auth │ │ │ + │ (no session) │ │ │ + ├────────────────>│ │ │ + │ │ │ │ + │ │ Check session │ │ + │ │ cookie │ │ + │ ├─────────────────>│ │ + │ │ (not found) │ │ + │ │<─────────────────│ │ + │ │ │ │ + │ Redirect to │ │ │ + │ connector │ │ │ + │<────────────────│ │ │ + │ │ │ │ + │ ... connector auth flow ... │ │ + │ │ │ │ + │ Callback with │ │ │ + │ identity │ │ │ + ├────────────────>│ │ │ + │ │ │ │ + │ │ Create/update │ │ + │ │ AuthSession │ │ + │ │ (ALWAYS) │ │ + │ ├─────────────────>│ │ + │ │ │ │ + │ Set-Cookie: │ │ │ + │ - Session cookie (no MaxAge) │ │ + │ if Remember Me unchecked │ │ + │ - Persistent cookie (with MaxAge) │ │ + │ if Remember Me checked │ │ + │ + redirect to /approval │ │ + │<────────────────│ │ │ + │ │ │ │ +``` + +**Key Point**: AuthSession is always created on successful authentication. The "Remember Me" checkbox only controls whether the cookie is a session cookie (deleted on browser close) or a persistent cookie (survives browser restart). This is consistent with Keycloak's behavior. + +#### SSO Flow (Returning User) + +``` +┌─────────┐ ┌─────────┐ ┌───────────┐ +│ Browser │ │ Dex │ │ Storage │ +└────â”Ŧ────┘ └────â”Ŧ────┘ └─────â”Ŧ─────┘ + │ │ │ + │ GET /auth │ │ + │ (with cookie) │ │ + │ client_id=B │ │ + ├────────────────>│ │ + │ │ │ + │ │ Get session │ + │ ├─────────────────>│ + │ │ (valid session) │ + │ │<─────────────────│ + │ │ │ + │ │ Check SSO │ + │ │ policy for │ + │ │ client B │ + │ │ │ + │ │ Check consent │ + │ │ for client B │ + │ ├─────────────────>│ + │ │ │ + │ If consented: │ │ + │ redirect with │ │ + │ code │ │ + │<────────────────│ │ + │ │ │ + │ If not: │ │ + │ show approval │ │ + │<────────────────│ │ + │ │ │ +``` + +#### prompt=none Flow + +``` +┌─────────┐ ┌─────────┐ ┌───────────┐ +│ Browser │ │ Dex │ │ Storage │ +└────â”Ŧ────┘ └────â”Ŧ────┘ └─────â”Ŧ─────┘ + │ │ │ + │ GET /auth │ │ + │ prompt=none │ │ + ├────────────────>│ │ + │ │ │ + │ │ Get session │ + │ ├─────────────────>│ + │ │ │ + │ If valid session + consent: │ + │ redirect with code │ + │<────────────────│ │ + │ │ │ + │ If no session or no consent: │ + │ redirect with error=login_required│ + │ or error=consent_required │ + │<────────────────│ │ + │ │ │ +``` + +#### Logout Flow + +``` +┌─────────┐ ┌─────────┐ ┌───────────┐ +│ Browser │ │ Dex │ │ Storage │ +└────â”Ŧ────┘ └────â”Ŧ────┘ └─────â”Ŧ─────┘ + │ │ │ + │ GET /logout │ │ + │ id_token_hint= │ │ + ├────────────────>│ │ + │ │ │ + │ │ Validate │ + │ │ id_token_hint │ + │ │ │ + │ │ Get identity │ + │ │ by session ID │ + │ ├─────────────────>│ + │ │ │ + │ │ Deactivate │ + │ │ (Active=false) │ + │ ├─────────────────>│ + │ │ │ + │ │ Revoke refresh │ + │ │ tokens │ + │ ├─────────────────>│ + │ │ │ + │ Clear cookie + │ │ + │ redirect or │ │ + │ show logout │ │ + │ confirmation │ │ + │<────────────────│ │ + │ │ │ +``` + +### Implementation Details/Notes/Constraints + +#### Feature Flag + +```go +// pkg/featureflags/set.go +var ( + // ...existing flags... + + // SessionsEnabled enables user sessions feature + SessionsEnabled = newFlag("sessions_enabled", false) +) +``` + +#### New Storage Entities + + +Two entities are required to properly handle the case where a user might be logged into different clients as different identities in the same browser: + +###### AuthSession + +```go +// storage/storage.go + +// AuthSession represents a browser's authentication state. +// One per browser, referenced by session cookie. +// Key: SessionID (random 32-byte string, stored in cookie) +type AuthSession struct { + // ID is the session identifier stored in cookie + ID string + + // ClientStates maps clientID → authentication state for that client + // Allows different users/identities per client in same browser + // + // Design note: This map-based approach is consistent with how OfflineSessions + // stores refresh tokens per client (OfflineSessions.Refresh map). Given that + // the number of OAuth clients in a typical deployment is bounded and relatively + // small (tens to hundreds, not thousands), the serialized size of this map + // will not exceed practical storage limits for any supported backend. + ClientStates map[string]*ClientAuthState + + // CreatedAt is when this browser session started + CreatedAt time.Time + + // LastActivity is when any client was last accessed + LastActivity time.Time + + // IPAddress at session creation (for audit) + IPAddress string + + // UserAgent at session creation (for audit) + UserAgent string +} + +// ClientAuthState represents authentication state for a specific client within an auth session. +// Expiration follows OIDC conventions with both absolute and idle timeout: +// - ExpiresAt enforces absolute lifetime (sessions.absoluteLifetime) +// - LastActivity + sessions.validIfNotUsedFor enforces idle timeout +// A client state is considered expired if EITHER condition is met. +type ClientAuthState struct { + // UserID + ConnectorID identify which UserIdentity is authenticated for this client + UserID string + ConnectorID string + + // Active indicates if authentication is active for this client + Active bool + + // ExpiresAt is the absolute expiration time for this client session. + // Set to time.Now() + absoluteLifetime at session creation. + // Cannot be extended - hard upper bound on session duration. + ExpiresAt time.Time + + // LastActivity is when this client session was last used (token issued, SSO check, etc.) + // Used with validIfNotUsedFor to enforce idle timeout. + // Updated on each request that touches this client state. + LastActivity time.Time + + // LastTokenIssuedAt is when a token was last issued for this client. + // Used for logout notifications and audit. + LastTokenIssuedAt time.Time +} +``` + +###### UserIdentity + +```go +// storage/storage.go + +// UserIdentity represents a user's persistent identity data. +// Stores data that persists across sessions: +// - Consent decisions +// - Future: 2FA enrollment +// +// Key: composite of UserID + ConnectorID (one per user per connector) +type UserIdentity struct { + // UserID is the subject identifier from the connector + UserID string + + // ConnectorID is the connector that authenticated the user + ConnectorID string + + // Claims holds the user's identity claims + // Updated on: + // 1. Each login (from connector callback) + // 2. Each refresh token usage (from RefreshConnector.Refresh) + // This ensures claims stay in sync with OfflineSessions and upstream IDP + Claims Claims + + // Consents stores user consent per client: map[clientID][]scopes + // Persists across sessions so user doesn't need to re-consent + Consents map[string][]string + + // CreatedAt is when this identity was first created + CreatedAt time.Time + + // LastLogin is when the user last authenticated (used for auth_time claim) + LastLogin time.Time + + // BlockedUntil is set when user is blocked from logging in + BlockedUntil time.Time + + // Future: 2FA fields + // TOTPSecret string + // WebAuthnCredentials []WebAuthnCredential +} +``` + +**Two-Entity Design Rationale** + +| Entity | Purpose | Lifecycle | Key | +|--------|---------|-----------|-----| +| AuthSession | Browser binding, per-client auth state | Short-lived (session timeout) | SessionID (cookie) | +| UserIdentity | User data, consents, 2FA | Long-lived (persists) | UserID + ConnectorID | + +**How It Works: Different Users in Different Clients** + +``` +Auth Session (cookie: dex_session=abc123) +├── ClientStates["client-A"]: +│ └── UserID: "alice", ConnectorID: "google", Active: true +├── ClientStates["client-B"]: +│ └── UserID: "bob", ConnectorID: "ldap", Active: true +└── ClientStates["client-C"]: + └── (empty - never authenticated) + +UserIdentity (alice + google): +├── Claims: {email: alice@example.com, ...} +├── Consents: {"client-A": ["openid", "email"]} +└── LastLogin: 2024-01-01 + +UserIdentity (bob + ldap): +├── Claims: {email: bob@corp.com, ...} +├── Consents: {"client-B": ["openid", "groups"]} +└── LastLogin: 2024-01-02 +``` + +**How SSO Works** + +When user accesses client-B with existing session: + +1. Get `AuthSession` by cookie +2. Check `ClientStates["client-B"]`: + - If exists and active → user already authenticated for this client +3. If not, check SSO: + - Find any `ClientStates[X]` where client-X has `ssoSharedWith` containing "client-B" + - If found → SSO! Copy auth state to `ClientStates["client-B"]` + - If not found → require authentication + +**SSO Session Lookup Algorithm** + +```go +// findSSOSession searches for a valid SSO source session for the target client +func (s *Server) findSSOSession(authSession *AuthSession, targetClientID string) (*ClientAuthState, *UserIdentity) { + targetClient, err := s.storage.GetClient(ctx, targetClientID) + if err != nil { + return nil, nil + } + + // Iterate through all active client states in this browser session + for sourceClientID, state := range authSession.ClientStates { + // Skip inactive or expired states + if !state.Active || time.Now().After(state.ExpiresAt) { + continue + } + + // Get the source client configuration + sourceClient, err := s.storage.GetClient(ctx, sourceClientID) + if err != nil { + continue + } + + // Check if source client shares its session with the target client + // SSO is allowed if: + // 1. Source client has ssoSharedWith: ["*"] (shares with everyone) + // 2. Source client has targetClientID in its ssoSharedWith list + if !s.clientSharesSessionWith(sourceClient, targetClientID) { + continue + } + + // Found a valid SSO source! Get the user identity + identity, err := s.storage.GetUserIdentity(ctx, state.UserID, state.ConnectorID) + if err != nil { + continue + } + + // Check if user is not blocked + if identity.BlockedUntil.After(time.Now()) { + continue + } + + return state, identity + } + + return nil, nil +} + +// clientSharesSessionWith checks if sourceClient shares its session with targetClientID +func (s *Server) clientSharesSessionWith(sourceClient Client, targetClientID string) bool { + for _, peer := range sourceClient.SSOSharedWith { + if peer == "*" || peer == targetClientID { + return true + } + } + return false +} +``` + +**SSO Lookup Flow Diagram** + +``` +User accesses client-B with existing session + │ + â–ŧ +┌─────────────────────────────────┐ +│ Get AuthSession from cookie │ +└─────────────────────────────────┘ + │ + â–ŧ +┌─────────────────────────────────┐ +│ Check ClientStates["client-B"] │ +│ exists and active? │ +└─────────────────────────────────┘ + │ │ + Yes No + │ │ + â–ŧ â–ŧ +┌──────────────┐ ┌─────────────────────────────────┐ +│ Use existing │ │ For each ClientStates[X]: │ +│ session │ │ - Is state active? │ +└──────────────┘ │ - Get client-X config │ + │ - Does client-X share with B? │ + │ (X.ssoSharedWith has B or *)│ + └─────────────────────────────────┘ + │ │ + Found match No match + │ │ + â–ŧ â–ŧ + ┌──────────────┐ ┌──────────────┐ + │ SSO! Copy │ │ Require │ + │ state to B │ │ authentication│ + └──────────────┘ └──────────────┘ +``` + +**Example: SSO Flow** + +``` +1. User logs into client-A as alice + AuthSession.ClientStates["client-A"] = {UserID: "alice", Active: true} + +2. User accesses client-B + - client-A.ssoSharedWith includes "client-B" ✓ + - SSO! Copy: ClientStates["client-B"] = {UserID: "alice", Active: true} + - Issue tokens for alice to client-B +``` + +**Example: No SSO, Different User** + +``` +1. User logged into client-A as alice + AuthSession.ClientStates["client-A"] = {UserID: "alice", Active: true} + +2. User accesses client-B (client-A does NOT share with client-B) + - No SSO available + - Redirect to connector for authentication + +3. User logs in as bob (different account) + AuthSession.ClientStates["client-B"] = {UserID: "bob", Active: true} + +Same browser, two different users, no conflict! +``` + +**Claims Synchronization with Refresh Tokens** + +When a refresh token is used: +1. `RefreshConnector.Refresh()` returns updated claims +2. Update `OfflineSessions.ConnectorData` (existing behavior) +3. **NEW**: Also update `UserIdentity.Claims`: + +```go +// In refresh token handler +func (s *Server) handleRefreshToken(...) { + // ...existing refresh logic... + + newIdentity, err := refreshConn.Refresh(ctx, scopes, oldIdentity) + if err != nil { + // Handle refresh failure + } + + // Update OfflineSessions (existing) + s.storage.UpdateOfflineSessions(...) + + // Update UserIdentity claims (NEW) + if s.sessionsEnabled { + s.storage.UpdateUserIdentity(ctx, newIdentity.UserID, connectorID, + func(u UserIdentity) (UserIdentity, error) { + u.Claims = storage.Claims{ + UserID: newIdentity.UserID, + Username: newIdentity.Username, + Email: newIdentity.Email, + Groups: newIdentity.Groups, + // ... + } + return u, nil + }) + } +} +``` + +This ensures `UserIdentity.Claims` stays synchronized with: +- Connector's current user data +- `OfflineSessions.ConnectorData` +- Actual refresh token claims + +**Why UserIdentity instead of AuthSession?** + +The name `UserIdentity` is chosen because this entity stores more than just session state: +1. **Persistent data**: Consent decisions survive session expiration +2. **Future 2FA**: TOTP secrets and WebAuthn credentials will be stored here +3. **One per user/connector**: Unlike sessions which could be per-browser, this is per-identity + +**Session ID Regeneration** + +The `AuthSession.ID` is regenerated when: +- User logs in from a new browser (new session created) +- Security concern requires new session (e.g., after password change) + +Individual `ClientStates` can be invalidated without changing the auth session ID. + +**Multiple Users in Same Browser** + +With the two-entity design: +- `AuthSession` tracks which user is authenticated for which client +- Different clients can have different users (if no SSO trust) +- Same user can be authenticated for multiple clients (SSO or separate logins) + +**SSO and Different Users** + +With SSO enabled between clients, the same user is used for all sharing clients: +- User logs in to client-A as "alice@example.com" +- User accesses client-B (client-A shares with client-B) → automatically authenticated as "alice@example.com" +- SSO reuses the identity from the sharing client + +If user needs to login as different identity to a sharing client: +- Use `prompt=login` to force re-authentication +- This creates new ClientState for that client with potentially different user + +Without SSO, user can be different identities in different clients (see examples above). + +#### Storage Interface Extensions + +Two new entities require CRUD operations: + +```go +// storage/storage.go + +type Storage interface { + // ...existing methods... + + // AuthSession management + CreateAuthSession(ctx context.Context, s AuthSession) error + GetAuthSession(ctx context.Context, sessionID string) (AuthSession, error) + UpdateAuthSession(ctx context.Context, sessionID string, updater func(s AuthSession) (AuthSession, error)) error + DeleteAuthSession(ctx context.Context, sessionID string) error + + // UserIdentity management + CreateUserIdentity(ctx context.Context, u UserIdentity) error + GetUserIdentity(ctx context.Context, userID, connectorID string) (UserIdentity, error) + UpdateUserIdentity(ctx context.Context, userID, connectorID string, updater func(u UserIdentity) (UserIdentity, error)) error + DeleteUserIdentity(ctx context.Context, userID, connectorID string) error + + // List for admin API + ListUserIdentities(ctx context.Context) ([]UserIdentity, error) +} +``` + +**Garbage Collection** + +```go +type GCResult struct { + // ...existing fields... + AuthSessions int64 // NEW: expired auth sessions cleaned up +} +``` + +`AuthSession` objects are garbage collected when: +- `LastActivity + validIfNotUsedFor` exceeded (inactivity) +- All `ClientStates` have expired + +`UserIdentity` objects are NOT garbage collected (preserve consents, future 2FA). + +#### Session Expiration + +**AuthSession expiration:** +- Entire session expires when `LastActivity + validIfNotUsedFor` is reached (idle timeout) +- On expiration, `AuthSession` is deleted by GC +- User must re-authenticate for all clients + +**ClientAuthState expiration (per-client within AuthSession):** + +Each client state enforces **both** absolute lifetime and idle timeout, consistent with standard OIDC session semantics: + +```go +func (s *Server) isClientStateValid(state *ClientAuthState) bool { + now := time.Now() + + // 1. Check absolute lifetime - hard upper bound, cannot be extended + if now.After(state.ExpiresAt) { + return false + } + + // 2. Check idle timeout - session unused for too long + if now.After(state.LastActivity.Add(s.sessionsConfig.validIfNotUsedFor)) { + return false + } + + // 3. Check explicit deactivation (admin revoked) + if !state.Active { + return false + } + + return true +} +``` + +When a client state expires: +- Other clients in same auth session remain active +- User must re-authenticate only for the expired client +- On successful re-authentication, a new `ClientAuthState` is created with fresh `ExpiresAt` + +**Admin can force re-authentication:** +- Delete `AuthSession` → user must re-auth for all clients +- Set `ClientStates[clientID].Active = false` → user must re-auth for that client only + +#### Deletion Risks + +**Deleting AuthSession:** +- User must re-authenticate for all clients +- No data loss (consents preserved in UserIdentity) +- Safe operation for logout + +**Deleting UserIdentity:** + +| What's Lost | Impact | +|-------------|--------| +| Consent decisions | User must re-approve scopes for all clients | +| Future: 2FA enrollment | User must re-enroll TOTP/WebAuthn | + +**When to delete UserIdentity:** +- User explicitly requests account deletion (GDPR) +- Admin cleanup of stale identities +- User removed from upstream identity provider + +**When NOT to delete (delete AuthSession instead):** +- Regular logout - delete AuthSession or set ClientState.Active = false +- Session expiration - GC handles AuthSession cleanup +- Security concern - delete AuthSession to force re-auth + +#### Session Cookie Format + +The session cookie contains only the session ID (not the session data): + +``` +Cookie: dex_session=; Path=; Secure; HttpOnly; SameSite=Lax +``` + +**Cookie Path**: Derived from the issuer URL path (`issuerURL.Path`). For example: +- Issuer: `https://dex.example.com/` → `Path=/` +- Issuer: `https://example.com/dex` → `Path=/dex` + +This is consistent with how Dex already handles routing - all endpoints are prefixed with the issuer path. + +**Session Creation vs Cookie Persistence (Keycloak-like behavior)** + +Unlike some implementations where "Remember Me" controls session creation, we follow Keycloak's approach: + +- **AuthSession is ALWAYS created** on successful authentication +- **"Remember Me" controls cookie persistence**: + - Unchecked: Session cookie (expires when browser closes) + - Checked: Persistent cookie (expires at `absoluteLifetime`) + +This approach is better because: +1. SSO works within a browser session even without "Remember Me" +2. Consent decisions are preserved during the browser session +3. `prompt=none` works correctly within browser session +4. More intuitive: "Remember Me" = "remember me after I close the browser" + +```go +func (s *Server) setSessionCookie(w http.ResponseWriter, sessionID string, rememberMe bool) { + cookie := &http.Cookie{ + Name: s.sessionsConfig.CookieName, + Value: sessionID, + Path: s.issuerURL.Path, + HttpOnly: true, + Secure: s.issuerURL.Scheme == "https", + SameSite: http.SameSiteLaxMode, + } + + if rememberMe { + // Persistent cookie - survives browser restart + cookie.MaxAge = int(s.sessionsConfig.absoluteLifetime.Seconds()) + } + // else: Session cookie - no MaxAge, browser deletes on close + + http.SetCookie(w, cookie) +} +``` + +Session ID generation: +```go +func NewSessionID() string { + return newSecureID(32) // 256-bit random value +} +``` + +#### Client Configuration Extension + +A new client configuration field is introduced for SSO control: + +```go +// storage/storage.go + +type Client struct { + // ...existing fields... + + // TrustedPeers are a list of peers which can issue tokens on this client's behalf. + // This is used for cross-client token issuance (existing behavior). + TrustedPeers []string `json:"trustedPeers" yaml:"trustedPeers"` + + // SSOSharedWith defines which other clients can reuse this client's authentication session. + // When a user is authenticated for this client, clients listed here can skip authentication. + // This is separate from TrustedPeers - organizations may want different policies for + // session sharing vs token delegation. + // Special value "*" means share with all clients (Keycloak-like realm-wide SSO). + // nil means use ssoSharedWithDefault from sessions config. + // Empty slice [] means explicitly share with no one. + SSOSharedWith []string `json:"ssoSharedWith,omitempty" yaml:"ssoSharedWith,omitempty"` +} +``` + +#### Connector Logout (Future) + +Logout URLs should be configured on connectors, not clients. A new connector interface will be added: + +```go +// connector/connector.go + +// LogoutConnector is an optional interface for connectors that support +// terminating upstream sessions on logout. +type LogoutConnector interface { + // Logout terminates the user's session at the upstream identity provider. + // Returns a URL to redirect the user to for upstream logout, or empty string + // if no redirect is needed. + Logout(ctx context.Context, connectorData []byte) (logoutURL string, err error) +} +``` + +Connectors that implement this interface (e.g., OIDC with `end_session_endpoint`, SAML with SLO): +- Are called during Dex logout flow +- Can redirect user to upstream for complete logout +- Implementation details are connector-specific + +This is tracked as a future improvement. + +#### Server Configuration Extension + +```go +// cmd/dex/config.go + +type Sessions struct { + // CookieName is the session cookie name (default: "dex_session") + CookieName string `json:"cookieName"` + + // AbsoluteLifetime is the maximum session lifetime (default: "24h") + AbsoluteLifetime string `json:"absoluteLifetime"` + + // ValidIfNotUsedFor is the inactivity timeout (default: "1h") + ValidIfNotUsedFor string `json:"validIfNotUsedFor"` + + // SSOSharedWithDefault is the default SSO sharing policy + // "all" = share with all clients, "none" = share with no one (default: "none") + SSOSharedWithDefault string `json:"ssoSharedWithDefault"` + + // RememberMeCheckedByDefault controls the initial checkbox state in templates + // true = pre-checked, false = unchecked (default: false) + RememberMeCheckedByDefault bool `json:"rememberMeCheckedByDefault"` +} +``` + +**Using ssoSharedWithDefault in SSO logic:** + +```go +func (s *Server) clientSharesSessionWith(sourceClient Client, targetClientID string) bool { + ssoSharedWith := sourceClient.SSOSharedWith + + // If client has no explicit ssoSharedWith, use default + if ssoSharedWith == nil { + switch s.sessionsConfig.SSOSharedWithDefault { + case "all": + return true // Share with everyone by default + default: // "none" + return false // Share with no one by default + } + } + + // Explicit configuration: empty slice means explicitly share with no one + // This is different from nil (not configured) + if len(ssoSharedWith) == 0 { + return false + } + + // Check explicit sharing list + for _, peer := range ssoSharedWith { + if peer == "*" || peer == targetClientID { + return true + } + } + return false +} +``` + +**Three states for ssoSharedWith:** +1. `nil` (not configured) → use `ssoSharedWithDefault` +2. `[]` (empty slice) → explicitly share with no one +3. `["client-a", ...]` or `["*"]` → explicit sharing list + +#### Prompt Parameter Handling + +Dex will support the following `prompt` values per OIDC Core specification: +- `none` - Silent authentication, no UI displayed +- `login` - Force re-authentication +- `consent` - Force consent screen +- Empty (default) - Normal flow with session reuse + +The `select_account` value is not supported initially (would require account linking feature). + +```go +func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) { + // ...existing parsing... + + prompt := r.Form.Get("prompt") + maxAge := r.Form.Get("max_age") + idTokenHint := r.Form.Get("id_token_hint") + clientID := r.Form.Get("client_id") + + // Get auth session from cookie + authSession, err := s.getAuthSessionFromCookie(r) + + // Get client auth state for this specific client + var clientState *ClientAuthState + var userIdentity *UserIdentity + if authSession != nil { + clientState = authSession.ClientStates[clientID] + if clientState != nil && clientState.Active { + userIdentity, _ = s.storage.GetUserIdentity(ctx, clientState.UserID, clientState.ConnectorID) + } + } + + // Handle max_age parameter (OIDC Core 3.1.2.1) + if maxAge != "" && userIdentity != nil { + maxAgeSeconds, err := strconv.Atoi(maxAge) + if err == nil && maxAgeSeconds >= 0 { + authAge := time.Since(userIdentity.LastLogin) + if authAge > time.Duration(maxAgeSeconds)*time.Second { + // Session is too old, force re-authentication + clientState = nil + userIdentity = nil + } + } + } + + switch prompt { + case "none": + // Silent authentication - must have valid session and consent + if clientState == nil || userIdentity == nil { + s.authErr(w, r, redirectURI, "login_required", state) + return + } + // Check consent in identity + consentedScopes, hasConsent := userIdentity.Consents[clientID] + if !hasConsent || !s.scopesCovered(consentedScopes, requestedScopes) { + s.authErr(w, r, redirectURI, "consent_required", state) + return + } + // Issue tokens without UI + + case "login": + // Force re-authentication - ignore existing session for this client + clientState = nil + userIdentity = nil + // Continue to connector login + + case "consent": + // Force consent screen even if previously consented + // Continue but don't check consent + + default: // "" - normal flow + // Check for SSO from trusted clients if no direct session + if clientState == nil && authSession != nil { + clientState, userIdentity = s.findSSOSession(authSession, clientID) + } + } + + // Validate id_token_hint if provided + if idTokenHint != "" { + claims, err := s.validateIDTokenHint(idTokenHint) + if err != nil { + s.authErr(w, r, redirectURI, "invalid_request", state) + return + } + if userIdentity != nil && userIdentity.UserID != claims.Subject { + // Identity user doesn't match hint + if prompt == "none" { + s.authErr(w, r, redirectURI, "login_required", state) + return + } + // Force re-login for different user + clientState = nil + userIdentity = nil + } + } + + // ...continue with flow... +} + +// findSSOSession looks for a valid SSO session from a sharing client +func (s *Server) findSSOSession(authSession *AuthSession, targetClientID string) (*ClientAuthState, *UserIdentity) { + for sourceClientID, state := range authSession.ClientStates { + if !state.Active { + continue + } + sourceClient, _ := s.storage.GetClient(ctx, sourceClientID) + if sourceClient == nil { + continue + } + // Check if source client shares its session with target client + if s.clientSharesSessionWith(sourceClient, targetClientID) { + identity, _ := s.storage.GetUserIdentity(ctx, state.UserID, state.ConnectorID) + if identity != nil { + return state, identity + } + } + } + return nil, nil +} +``` + +**max_age Parameter** + +The `max_age` parameter is supported per OIDC Core specification: +- Specifies the maximum authentication age in seconds +- If the identity's last authentication time (`LastLogin`) exceeds `max_age`, force re-authentication +- When `max_age` is used, the `auth_time` claim MUST be included in the ID token + +#### New Endpoints + +``` +POST /logout +GET /logout +``` + +Logout endpoint following the OpenID RP-Initiated Logout specification ([OpenID spec](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)): + +```go +func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { + idTokenHint := r.FormValue("id_token_hint") + postLogoutRedirectURI := r.FormValue("post_logout_redirect_uri") + state := r.FormValue("state") + clientID := r.FormValue("client_id") // Optional: logout from specific client + + // Get auth session from cookie + authSession, _ := s.getAuthSessionFromCookie(r) + + // Validate id_token_hint if provided + var hintUserID, hintConnectorID string + if idTokenHint != "" { + claims, err := s.validateIDTokenHint(idTokenHint) + if err == nil { + hintUserID = claims.Subject + // Extract connector from token if possible + } + } + + if authSession != nil { + if clientID != "" { + // Logout from specific client only + delete(authSession.ClientStates, clientID) + s.storage.UpdateAuthSession(ctx, authSession.ID, ...) + } else { + // Logout from all clients - delete entire auth session + s.storage.DeleteAuthSession(ctx, authSession.ID) + } + + // Revoke refresh tokens for logged-out clients + // ... + } + + // Clear cookie and redirect + s.clearSessionCookie(w) + + // Show logout confirmation or redirect + if postLogoutRedirectURI != "" && s.isValidPostLogoutURI(postLogoutRedirectURI, idTokenHint) { + u, _ := url.Parse(postLogoutRedirectURI) + if state != "" { + q := u.Query() + q.Set("state", state) + u.RawQuery = q.Encode() + } + http.Redirect(w, r, u.String(), http.StatusFound) + return + } + + // Show logout confirmation page + s.templates.logout(w, r) +} +``` + +**Future: Upstream Connector Logout** + +For CallbackConnectors (OIDC, OAuth, SAML), the upstream identity provider may also have an active session. Future work should include: +- Implement `LogoutConnector` interface (see above) +- OIDC connectors use `end_session_endpoint` from discovery +- SAML connectors use Single Logout (SLO) +- Redirect user to upstream after Dex logout + +This is tracked as a future improvement. + +#### Discovery Updates + +```go +func (s *Server) constructDiscovery(ctx context.Context) discovery { + d := discovery{ + // ...existing fields... + } + + if s.sessionsEnabled { + d.EndSessionEndpoint = s.absURL("/logout") + } + + return d +} +``` + +#### Login Template Updates + +When sessions are enabled, add "Remember Me" checkbox to authentication flow. + +**Template Data** + +The server passes these values to templates: + +```go +type templateData struct { + // ...existing fields... + + // SessionsEnabled indicates if sessions feature is active + SessionsEnabled bool + + // RememberMeChecked is the default checkbox state + // Set from config: sessions.rememberMeCheckedByDefault + RememberMeChecked bool +} +``` + +**For PasswordConnector (login form exists in Dex):** + +```html + +
+ + + {{ if .SessionsEnabled }} +
+ + +
+ {{ end }} + + +
+``` + +**For CallbackConnector (no login form in Dex):** + +For OAuth/OIDC/SAML connectors, the user is redirected to upstream IDP and there's no Dex login form. + +**Show on Approval Page** (recommended): Add "Remember Me" checkbox to the approval/consent page. User sees it after returning from upstream IDP, before granting consent. + +```html + +
+ + + {{ if .SessionsEnabled }} +
+ + +
+ {{ end }} + + +
+``` + +**When skipApprovalScreen is true**: If approval screen is skipped, the `rememberMeCheckedByDefault` config determines cookie persistence: +- `false` (default): Session cookie (deleted on browser close) +- `true`: Persistent cookie (survives browser restart) + +**Remember Me Behavior** (Keycloak-like): +- **AuthSession is ALWAYS created** on successful authentication regardless of checkbox +- **Checkbox controls cookie persistence only**: + - **Unchecked**: Session cookie - expires when browser closes. SSO works within browser session. + - **Checked**: Persistent cookie - survives browser restart until `absoluteLifetime` expires. + +#### Connector Type Considerations + +**CallbackConnector** (OIDC, OAuth, SAML, GitHub, etc.): +- Session created after successful callback +- Upstream tokens stored in refresh token's ConnectorData (not in session) +- Identity refresh via RefreshConnector when refresh token is used + +**PasswordConnector** (LDAP, local passwords): +- Session created after successful password verification +- No upstream tokens +- Identity refresh re-validates against password backend when refresh token is used + +Both types work the same way with sessions - the connector type only affects: +1. Initial authentication flow (redirect vs password form) +2. How identity refresh works (via refresh tokens, not sessions) + +#### Connector Configuration Changes + +Sessions reference a `ConnectorID`, but connector configuration may change after session creation (e.g., OIDC issuer URL changes, LDAP server replaced, connector removed entirely). + +**Behavior**: Dex does NOT automatically invalidate sessions when connector configuration changes. This is by design - Dex has no mechanism to detect configuration changes at runtime, and connectors are typically reconfigured during planned maintenance. + +**Administrator responsibility**: When connector configuration changes in a way that invalidates existing user identities (e.g., connector removed, upstream IdP replaced), administrators should: +1. Terminate affected sessions via gRPC admin API (future: `DexSessions.TerminateByConnector(connectorID)`) +2. Or wait for sessions to expire naturally +3. Or restart Dex with `DEX_SESSIONS_ENABLED=false` temporarily to force re-authentication + +If a session references a connector that no longer exists, the session will fail gracefully at the next use: `GetConnector()` will return an error, and the user will be redirected to authenticate again. + +### Risks and Mitigations + +#### Security Risks + +| Risk | Mitigation | +|------|------------| +| Session hijacking | Secure cookie flags (HttpOnly, Secure, SameSite), short idle timeout | +| Session fixation | Generate new session ID after authentication (see below) | +| CSRF on logout | GET shows confirmation page, POST performs logout | +| Cookie theft | Bind session to fingerprint (IP range, partial user agent) - optional | +| Storage exposure | Session IDs are random 256-bit values, no sensitive data in cookie | + +**Session Fixation Protection** + +Session fixation attacks occur when an attacker sets a known session ID in a victim's browser before authentication, then hijacks the session after the victim logs in. + +References: +- [OWASP Session Fixation](https://owasp.org/www-community/attacks/Session_fixation) +- [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) + +**Mitigations implemented:** + +1. **Regenerate session ID on authentication**: When a user successfully authenticates, ALWAYS generate a new `AuthSession.ID` even if a session already exists. Never reuse a pre-authentication session ID. + +```go +// This is not the real method signature, but the implementation example of a specific behavior. +func (s *Server) onSuccessfulAuthentication(w http.ResponseWriter, userID, connectorID, clientID string, rememberMe bool) { + // ALWAYS generate new session ID - prevents session fixation + newSessionID := NewSessionID() + + // Create or update AuthSession with NEW ID + authSession := &AuthSession{ + ID: newSessionID, // Always new, never reuse + ClientStates: make(map[string]*ClientAuthState), + CreatedAt: time.Now(), + // ... + } + + // Set cookie with new session ID + s.setSessionCookie(w, newSessionID, rememberMe) +} +``` + +2. **Don't accept session IDs from URL parameters**: Session IDs are ONLY accepted from cookies, never from query parameters or POST data. + +3. **Strict cookie settings**: `HttpOnly`, `Secure`, `SameSite=Lax` prevent common session theft vectors. + +4. **Session binding (optional future enhancement)**: Bind session to client characteristics (IP range, user agent) to detect stolen cookies. + +**Handling existing sessions during authentication:** + +When a user authenticates and an existing `AuthSession` is found: +1. Generate a completely new session ID +2. Copy relevant state from old session to new session (if any) +3. Delete the old `AuthSession` from storage +4. Set cookie with new session ID + +This ensures that even if an attacker set a session cookie before authentication, they cannot use it after the victim logs in. + +#### Operational Risks + +| Risk | Mitigation | +|------|------------| +| Storage growth | AuthSessions are GC'd on inactivity; UserIdentities are per-user like OfflineSessions; admin API allows cleanup | +| Storage performance | Additional read per request to resolve session cookie. Impact depends on backend — see note below | +| Migration complexity | Feature flag allows gradual rollout, no breaking changes | + +**Storage Performance Note** + +Enabling sessions introduces an additional storage read on each authorization request (to resolve the session cookie to an `AuthSession`). The actual performance impact depends on the storage backend: + +- **SQL (Postgres, MySQL, SQLite)**: Session lookup by primary key is a single indexed read — negligible overhead +- **etcd**: Single key-value lookup — negligible overhead +- **Kubernetes CRDs**: GET by resource name — slightly higher latency than SQL/etcd but still within acceptable bounds (may require [priority&fairness](https://kubernetes.io/docs/concepts/cluster-administration/flow-control/) tuning) +- **Memory**: In-process map lookup — no overhead + +At this stage, we do not have production metrics to quantify the exact impact. The storage access pattern is identical to existing `OfflineSessions` lookups (single record by key), which are already proven in production. It is recommended to monitor storage latency after enabling sessions and adjusting `validIfNotUsedFor` if the GC frequency needs tuning. + +#### Breaking Changes + +**None** - Sessions are opt-in via feature flag and configuration. Existing deployments continue to work without changes. + +#### Rollback Plan + +Sessions are fully controlled by the `DEX_SESSIONS_ENABLED` feature flag. Rollback is straightforward: + +1. **Disable feature flag**: Set `DEX_SESSIONS_ENABLED=false` (or remove it) +2. **Immediate effect**: Dex stops creating, reading, and validating sessions. All authorization requests proceed as before sessions were introduced — connector authentication on every request, no SSO, no session cookies +3. **Cookie cleanup**: Existing session cookies in browsers become inert — Dex ignores them when sessions are disabled. They expire naturally per their MaxAge or when the browser is closed +4. **Storage cleanup**: `AuthSession` and `UserIdentity` records remain in storage but are unused. They can be cleaned up manually or left to accumulate no further growth +5. **No downtime required**: Feature flag can be toggled without restart if environment variable reload is supported; otherwise, a rolling restart is sufficient + +**Key guarantee**: Disabling the feature flag returns Dex to its pre-sessions behavior with zero side effects. No existing functionality (refresh tokens, connector authentication, token issuance) depends on sessions. Additional tables in the database cost nothing when the feature flag is disabled: they remain unused schema objects and can be deleted later if desired. + +#### Migration Path + +1. Deploy new Dex version - storage migrations create `AuthSession` and `UserIdentity` tables/resources automatically (no feature flag needed for schema) +2. Enable feature flag `DEX_SESSIONS_ENABLED=true` when ready to use sessions +3. Add `sessions:` configuration block +4. Sessions start being created for all new logins; "Remember Me" controls cookie persistence (session vs persistent cookie) +5. Existing refresh tokens continue to work + +**Note**: Storage schema changes (new tables/CRDs) are applied on startup regardless of feature flag. The feature flag only controls whether sessions are actually created and used. This simplifies deployment - you can deploy the new version, then enable sessions later without another deployment. + +### Alternatives + +#### 1. Stateless Sessions (JWT in Cookie) + +**Approach**: Store session data directly in a signed/encrypted JWT cookie. + +**Pros**: +- No server-side storage required +- Scales horizontally without shared state + +**Cons**: +- Cannot revoke sessions without blocklist +- Cookie size limits (~4KB) +- Cannot store consent history or client tracking for logout +- No server-side session list for logout + +**Decision**: Rejected. Server-side sessions are required for proper logout and SSO. + +#### 2. Extend OfflineSessions + +**Approach**: Add session data to existing OfflineSessions entity. + +**Pros**: +- Reuses existing storage +- Simpler migration + +**Cons**: +- OfflineSessions are per-connector, not per-browser +- Different lifecycle (refresh token vs browser session) +- Would complicate existing OfflineSessions logic + +**Decision**: Rejected. Clean separation is better for maintainability. + +#### 3. External Session Store (Redis) + +**Approach**: Use Redis for session storage instead of existing backends. + +**Pros**: +- Built-in TTL support +- Fast reads/writes +- Proven session store + +**Cons**: +- Adds infrastructure dependency +- Against Dex's simplicity philosophy +- Doesn't work with Kubernetes CRD backend + +**Decision**: Rejected. Must work with existing storage backends. + +#### 4. Do Nothing + +**Approach**: Keep using refresh tokens as implicit sessions. + +**Cons**: +- Cannot implement OIDC conformance features +- No proper SSO +- No proper logout +- Blocks future features (2FA, etc.) + +**Decision**: Rejected. These features are essential for enterprise adoption. + +## Future Improvements + +1. **Identity Refresh for Long-Lived Sessions** + - Periodic refresh of user identity from connector during active session + - Configurable refresh interval + - Refresh on token request option + - Handle connector revocation (terminate session) + +2. **Upstream Connector Logout** + - Redirect to upstream IDP logout endpoint after Dex logout + - Support RP-Initiated Logout towards upstream OIDC providers + - SAML Single Logout (SLO) support + - Configurable per-connector logout URLs + +3. **Session Introspection Endpoint** + - Implement session check endpoint similar to [RFC 7662 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662) + - Could enable replacing OAuth2 Proxy in some deployments + - Endpoint: `GET /session/introspect` or similar + - Returns session validity and user claims + - Useful for reverse proxies to validate session cookies directly + +4. **Front-Channel Logout** + - Implement [OIDC Front-Channel Logout 1.0](https://openid.net/specs/openid-connect-frontchannel-1_0.html) + - Notify client applications when user logs out via iframes + - Requires client `logoutURL` configuration + +5. **2FA/MFA Support** + - Store TOTP secrets in user profile + - Add MFA enrollment flow + - Step-up authentication for sensitive operations + - WebAuthn/Passkey support + +6. **Session Management API** + - List active sessions via gRPC API + - Revoke sessions via gRPC API + - Session activity audit log + +7. **Back-Channel Logout** + - Implement [OIDC Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) + - Server-to-server logout notifications + +8. **Account Linking** + - Link multiple connector identities to single user + - Switch between linked identities + +9. **Device/Session Fingerprinting** + - Optional session binding to client characteristics + - Anomaly detection for session theft + +10. **Per-Connector Session Policies** + - Different session lifetimes per connector + - Different SSO policies per connector + +11. **Session Impersonation for Admin** + - Admin can impersonate user sessions for debugging + - Audit logging for impersonation + +12. **Consent Management UI** + - User-facing page to view/revoke consents + - GDPR compliance features + diff --git a/docs/enhancements/cel-expressions-2026-02-28.md b/docs/enhancements/cel-expressions-2026-02-28.md new file mode 100644 index 0000000000..efd2831f7e --- /dev/null +++ b/docs/enhancements/cel-expressions-2026-02-28.md @@ -0,0 +1,732 @@ +# Dex Enhancement Proposal (DEP) - 2026-02-28 - CEL (Common Expression Language) Integration + +## Table of Contents + +- [Summary](#summary) +- [Context](#context) +- [Motivation](#motivation) + - [Goals/Pain](#goalspain) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) + - [User Experience](#user-experience) + - [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints) + - [Phase 1: pkg/cel - Core CEL Library](#phase-1-pkgcel---core-cel-library) + - [Phase 2: Authentication Policies](#phase-2-authentication-policies) + - [Phase 3: Token Policies](#phase-3-token-policies) + - [Phase 4: OIDC Connector Claim Mapping](#phase-4-oidc-connector-claim-mapping) + - [Policy Application Flow](#policy-application-flow) + - [Risks and Mitigations](#risks-and-mitigations) + - [Alternatives](#alternatives) +- [Future Improvements](#future-improvements) + +## Summary + +This DEP proposes integrating [CEL (Common Expression Language)][cel-spec] into Dex as a first-class +expression engine for policy evaluation, claim mapping, and token customization. A new reusable +`pkg/cel` package will provide a safe, sandboxed CEL environment with Kubernetes-grade compatibility +guarantees, cost budgets, and a curated set of extension libraries. Subsequent phases will leverage +this package to implement authentication policies, token policies, advanced claim mapping in +connectors, and per-client/global access rules — replacing the need for ad-hoc configuration fields +and external policy engines. + +[cel-spec]: https://github.com/google/cel-spec + +## Context + +- [#1583 Add allowedGroups option for clients config][#1583] — a long-standing request for a + configuration option to allow a client to specify a list of allowed groups. +- [#1635 Connector Middleware][#1635] — long-standing request for a policy/middleware layer between + connectors and the server for claim transformations and access control. +- [#1052 Allow restricting connectors per client][#1052] — frequently requested feature to restrict + which connectors are available to specific OAuth2 clients. +- [#2178 Custom claims in ID tokens][#2178] — requests for including additional payload in issued tokens. +- [#2812 Token Exchange DEP][dep-token-exchange] — mentions CEL/Rego as future improvement for + policy-based assertions on exchanged tokens. +- The OIDC connector already has a growing set of ad-hoc claim mutation options + (`ClaimMapping`, `ClaimMutations.NewGroupFromClaims`, `FilterGroupClaims`, `ModifyGroupNames`) + that would benefit from a unified expression language. +- Previous community discussions explored OPA/Rego and JMESPath, but CEL offers a better fit + (see [Alternatives](#alternatives)). + +[#1583]: https://github.com/dexidp/dex/pull/1583 +[#1635]: https://github.com/dexidp/dex/issues/1635 +[#1052]: https://github.com/dexidp/dex/issues/1052 +[#2178]: https://github.com/dexidp/dex/issues/2178 +[dep-token-exchange]: /docs/enhancements/token-exchange-2023-02-03-%232812.md + +## Motivation + +### Goals/Pain + +1. **Complex query/filter capabilities** — Dex needs a way to express complex validations and + mutations in multiple places (authentication flow, token issuance, claim mapping). Today each + feature requires new Go code, new config fields, and a new release cycle. CEL allows operators + to express these rules declaratively without code changes. + +2. **Authentication policies** — Operators want to control _who_ can log in based on rich + conditions: restrict specific connectors to specific clients, require group membership for + certain clients, deny login based on email domain, enforce MFA claims, etc. Currently there is + no unified mechanism; users rely on downstream applications or external proxies. + +3. **Token policies** — Operators want to customize issued tokens: add extra claims to ID tokens, + restrict scopes per client, modify `aud` claims, include upstream connector metadata, etc. + Today this requires forking Dex or using a reverse proxy. + +4. **Claim mapping in OIDC connector** — The OIDC connector has accumulated multiple ad-hoc config + options for claim mapping and group mutations (`ClaimMapping`, `NewGroupFromClaims`, + `FilterGroupClaims`, `ModifyGroupNames`). A single CEL expression field would replace all of + these with a more powerful and composable approach. + +5. **Per-client and global policies** — One of the most frequent requests is allowing different + connectors for different clients and restricting group-based access per client. CEL policies at + the global and per-client level address this cleanly. + +6. **CNCF ecosystem alignment** — CEL has massive adoption across the CNCF ecosystem: + + | Project | CEL Usage | Evidence | + |---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| + | **Kubernetes** | ValidatingAdmissionPolicy, CRD validation rules (`x-kubernetes-validations`), AuthorizationPolicy, field selectors, CEL-based match conditions in webhooks | [KEP-3488][k8s-cel-kep], [CRD Validation Rules][k8s-crd-cel], [AuthorizationPolicy KEP-3221][k8s-authz-cel] | + | **Kyverno** | CEL expressions in validation/mutation policies (v1.12+), preconditions | [Kyverno CEL docs][kyverno-cel] | + | **OPA Gatekeeper** | Partially added support for CEL in constraint templates | [Gatekeeper CEL][gatekeeper-cel] | + | **Istio** | AuthorizationPolicy conditions, request routing, telemetry | [Istio CEL docs][istio-cel] | + | **Envoy / Envoy Gateway** | RBAC filter, ext_authz, rate limiting, route matching, access logging | [Envoy CEL docs][envoy-cel] | + | **Tekton** | Pipeline when expressions, CEL custom tasks | [Tekton CEL Interceptor][tekton-cel] | + | **Knative** | Trigger filters using CEL expressions | [Knative CEL filters][knative-cel] | + | **Google Cloud** | IAM Conditions, Cloud Deploy, Security Command Center | [Google IAM CEL][gcp-cel] | + | **Cert-Manager** | CertificateRequestPolicy approval using CEL | [cert-manager approver-policy CEL][cert-manager-cel] | + | **Cilium** | Hubble CEL filter logic | [Cilium CEL docs][cilium-cel] | + | **Crossplane** | Composition functions with CEL-based patch transforms | [Crossplane CEL transforms][crossplane-cel] | + | **Kube-OVN** | Network policy extensions using CEL | [Kube-OVN CEL][kube-ovn-cel] | + + [k8s-cel-kep]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/3488-cel-admission-control + [k8s-crd-cel]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules + [k8s-authz-cel]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/3221-structured-authorization-configuration + [kyverno-cel]: https://kyverno.io/docs/writing-policies/cel/ + [gatekeeper-cel]: https://open-policy-agent.github.io/gatekeeper/website/docs/validating-admission-policy/#policy-updates-to-add-vap-cel + [istio-cel]: https://istio.io/latest/docs/reference/config/security/conditions/ + [envoy-cel]: https://www.envoyproxy.io/docs/envoy/latest/xds/type/v3/cel.proto + [tekton-cel]: https://tekton.dev/docs/triggers/cel_expressions/ + [knative-cel]: https://github.com/knative/eventing/blob/main/docs/broker/filtering.md#add-cel-expression-filter + [gcp-cel]: https://cloud.google.com/iam/docs/conditions-overview + [cert-manager-cel]: https://cert-manager.io/docs/policy/approval/approver-policy/#validations + [cilium-cel]: https://docs.cilium.io/en/stable/_api/v1/flow/README/#flowfilter-experimental + [crossplane-cel]: https://github.com/crossplane-contrib/function-cel-filter + [kube-ovn-cel]: https://kubeovn.github.io/docs/stable/en/advance/cel-expression/ + + By choosing CEL, Dex operators who already use Kubernetes or other CNCF tools can reuse their + existing knowledge of the expression language. + +### Non-Goals + +- **Full policy engine** — This DEP does not aim to replace dedicated external policy engines + (OPA, Kyverno). CEL in Dex is scoped to identity and token operations. +- **Breaking changes to existing configuration** — All existing config fields (`ClaimMapping`, + `ClaimMutations`, etc.) will continue to work. CEL expressions are additive/opt-in. +- **Authorization (beyond Dex scope)** — Dex is an identity provider; downstream authorization + decisions remain the responsibility of relying parties. CEL policies in Dex are limited to + authentication and token issuance concerns. +- **Multi-phase CEL in a single DEP** — Only Phase 1 (`pkg/cel` package) is targeted for + immediate implementation. Phases 2-4 are included here for design context and will have their + own implementation PRs. +- **Multi-step logic** — CEL in Dex is scoped to single-expression evaluation. Each expression + is a standalone, stateless computation with no intermediate variables, chaining, or + multi-step transformations. If a use case requires sequential logic or conditionally chained + expressions, it belongs outside Dex (e.g. in an external policy engine or middleware). + This boundary protects the design from scope creep that pushes CEL beyond what it's good at. + +## Proposal + +### User Experience + +#### Authentication Policy (Phase 2) + +Operators can define global and per-client authentication policies in the Dex config: + +```yaml +# Global authentication policy — each expression evaluates to bool. +# If true — the request is denied. Evaluated in order; first match wins. +authPolicy: + - expression: "!identity.email.endsWith('@example.com')" + message: "'Login restricted to example.com domain'" + - expression: "!identity.email_verified" + message: "'Email must be verified'" + +staticClients: + - id: admin-app + name: Admin Application + secret: ... + redirectURIs: [...] + # Per-client policy — same structure as global + authPolicy: + - expression: "!(request.connector_id in ['okta', 'ldap'])" + message: "'This application requires Okta or LDAP login'" + - expression: "!('admin' in identity.groups)" + message: "'Admin group membership required'" +``` + +#### Token Policy (Phase 3) + +Operators can add extra claims or mutate token contents: + +```yaml +tokenPolicy: + # Global mutations applied to all ID tokens + claims: + # Add a custom claim based on group membership + - key: "'role'" + value: "identity.groups.exists(g, g == 'admin') ? 'admin' : 'user'" + # Include connector ID as a claim + - key: "'idp'" + value: "request.connector_id" + # Add department from upstream claims (only if present) + - key: "'department'" + value: "identity.extra['department']" + condition: "'department' in identity.extra" + +staticClients: + - id: internal-api + name: Internal API + secret: ... + redirectURIs: [...] + tokenPolicy: + claims: + - key: "'custom-claim.company.com/team'" + value: "identity.extra['team'].orValue('engineering')" + # Only add on-call claim for ops group members + - key: "'on_call'" + value: "true" + condition: "identity.groups.exists(g, g == 'ops')" + # Restrict scopes + filter: + expression: "request.scopes.all(s, s in ['openid', 'email', 'profile'])" + message: "'Unsupported scope requested'" +``` + +#### OIDC Connector Claim Mapping (Phase 4) + +Replace ad-hoc claim mapping with CEL: + +```yaml +connectors: + - type: oidc + id: corporate-idp + name: Corporate IdP + config: + issuer: https://idp.example.com + clientID: dex-client + clientSecret: ... + # CEL-based claim mapping — replaces claimMapping and claimModifications + claimMappingExpressions: + username: "claims.preferred_username.orValue(claims.email)" + email: "claims.email" + groups: > + claims.groups + .filter(g, g.startsWith('dex:')) + .map(g, g.trimPrefix('dex:')) + emailVerified: "claims.email_verified.orValue(true)" + # Extra claims to pass through to token policies + extra: + department: "claims.department.orValue('unknown')" + cost_center: "claims.cost_center.orValue('')" +``` + +### Implementation Details/Notes/Constraints + +### Phase 1: `pkg/cel` — Core CEL Library + +This is the foundation that all subsequent phases build upon. The package provides a safe, +reusable CEL environment with Kubernetes-grade guarantees. + +#### Package Structure + +``` +pkg/ + cel/ + cel.go # Core Environment, compilation, evaluation + types.go # CEL type declarations (Identity, Request, etc.) + cost.go # Cost estimation and budgeting + doc.go # Package documentation + library/ + email.go # Email-related CEL functions + groups.go # Group-related CEL functions +``` + +#### Dependencies + +``` +github.com/google/cel-go v0.27.0 +``` + +The `cel-go` library is the canonical Go implementation maintained by Google, used by Kubernetes +and all major CNCF projects. It follows semantic versioning and provides strong backward +compatibility guarantees. + +#### Core API Design + +**Public types:** + +```go +// CompilationResult holds a compiled CEL program ready for evaluation. +type CompilationResult struct { + Program cel.Program + OutputType *cel.Type + Expression string +} + +// Compiler compiles CEL expressions against a specific environment. +type Compiler struct { /* ... */ } + +// CompilerOption configures a Compiler. +type CompilerOption func(*compilerConfig) +``` + +**Compilation pipeline:** + +Each `Compile*` call performs these steps sequentially: +1. Reject expressions exceeding `MaxExpressionLength` (10,240 chars). +2. Compile and type-check the expression via `cel-go`. +3. Validate output type matches the expected type (for typed variants). +4. Estimate cost using `defaultCostEstimator` with size hints — reject if estimated max cost + exceeds the cost budget. +5. Create an optimized `cel.Program` with runtime cost limit. + +Presence tests (`has(field)`, `'key' in map`) have zero cost, matching Kubernetes CEL behavior. + +#### Variable Declarations + +Variables are declared via `VariableDeclaration{Name, Type}` and registered with `NewCompiler`. +Helper constructors provide pre-defined variable sets: + +**`IdentityVariables()`** — the `identity` variable (from `connector.Identity`), +typed as `cel.ObjectType`: + +| Field | CEL Type | Source | +|-------|----------|--------| +| `identity.user_id` | `string` | `connector.Identity.UserID` | +| `identity.username` | `string` | `connector.Identity.Username` | +| `identity.preferred_username` | `string` | `connector.Identity.PreferredUsername` | +| `identity.email` | `string` | `connector.Identity.Email` | +| `identity.email_verified` | `bool` | `connector.Identity.EmailVerified` | +| `identity.groups` | `list(string)` | `connector.Identity.Groups` | + +**`RequestVariables()`** — the `request` variable (from `RequestContext`), +typed as `cel.ObjectType`: + +| Field | CEL Type | +|-------|----------| +| `request.client_id` | `string` | +| `request.connector_id` | `string` | +| `request.scopes` | `list(string)` | +| `request.redirect_uri` | `string` | + +**`ClaimsVariable()`** — the `claims` variable for raw upstream claims as `map(string, dyn)`. + +**Typing strategy:** + +`identity` and `request` use `cel.ObjectType` with explicitly declared fields. This gives +compile-time type checking: a typo like `identity.emial` is rejected at config load time +rather than silently evaluating to null in production — critical for an auth system where a +misconfigured policy could lock users out. + +`claims` remains `map(string, dyn)` because its shape is genuinely unknown — it carries +arbitrary upstream IdP data. + +#### Compatibility Guarantees + +Following the Kubernetes CEL compatibility model +([KEP-3488: CEL for Admission Control][kep-3488], [Kubernetes CEL Migration Guide][k8s-cel-compat]): + +1. **Environment versioning** — The CEL environment is versioned. When new functions or variables + are added, they are introduced under a new environment version. Existing expressions compiled + against an older version continue to work. + + ```go + // EnvironmentVersion represents the version of the CEL environment. + // New variables, functions, or libraries are introduced in new versions. + type EnvironmentVersion uint32 + + const ( + // EnvironmentV1 is the initial CEL environment. + EnvironmentV1 EnvironmentVersion = 1 + ) + + // WithVersion sets the target environment version for the compiler. + func WithVersion(v EnvironmentVersion) CompilerOption + ``` + + This is directly modeled on `k8s.io/apiserver/pkg/cel/environment`. + +2. **Library stability** — Custom functions in the `pkg/cel/library` subpackage follow these rules: + - Functions MUST NOT be removed once released. + - Function signatures MUST NOT change once released. + - New functions MUST be added under a new `EnvironmentVersion`. + - If a function needs to be replaced, the old one is deprecated but kept forever. + +3. **Type stability** — CEL types (`Identity`, `Request`, `Claims`) follow the same rules: + - Fields MUST NOT be removed. + - Field types MUST NOT change. + - New fields are added in a new `EnvironmentVersion`. + +4. **Semantic versioning of `cel-go`** — The `cel-go` dependency follows semver. Dex pins to a + minor version range and updates are tested for behavioral changes. This is exactly the approach + Kubernetes takes: `k8s.io/apiextensions-apiserver` pins `cel-go` and gates new features behind + environment versions. + +5. **Feature gates** — New CEL-powered features are gated behind Dex feature flags (using the + existing `pkg/featureflags` mechanism) during their alpha phase. + +[kep-3488]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/3488-cel-admission-control +[k8s-cel-compat]: https://kubernetes.io/docs/reference/using-api/cel/ + +#### Cost Estimation and Budgets + +Like Kubernetes, Dex CEL expressions must be bounded to prevent denial-of-service. + +**Constants:** + +| Constant | Value | Description | +|----------|-------|-------------| +| `DefaultCostBudget` | `10_000_000` | Max cost units per evaluation (aligned with Kubernetes) | +| `MaxExpressionLength` | `10_240` | Max expression string length in characters | +| `DefaultStringMaxLength` | `256` | Estimated max string size for cost estimation | +| `DefaultListMaxLength` | `100` | Estimated max list size for cost estimation | + +**How it works:** + +A `defaultCostEstimator` (implementing `checker.CostEstimator`) provides size hints for known +variables (`identity`, `request`, `claims`) so the `cel-go` cost estimator doesn't assume +unbounded sizes. It also provides call cost estimates for custom Dex functions +(`dex.emailDomain`, `dex.emailLocalPart`, `dex.groupMatches`, `dex.groupFilter`). + +Expressions are validated at three levels: +1. **Length check** — reject expressions exceeding `MaxExpressionLength`. +2. **Compile-time cost estimation** — reject expressions whose estimated max cost exceeds + the cost budget. +3. **Runtime cost limit** — abort evaluation if actual cost exceeds the budget. + +#### Extension Libraries + +The `pkg/cel` environment includes these cel-go standard extensions (same set as Kubernetes): + +| Library | Description | Examples | +|---------|-------------|---------| +| `ext.Strings()` | Extended string functions | `"hello".upperAscii()`, `"foo:bar".split(':')`, `s.trim()`, `s.replace('a','b')` | +| `ext.Encoders()` | Base64 encoding/decoding | `base64.encode(bytes)`, `base64.decode(str)` | +| `ext.Lists()` | Extended list functions | `list.slice(1, 3)`, `list.flatten()` | +| `ext.Sets()` | Set operations on lists | `sets.contains(a, b)`, `sets.intersects(a, b)`, `sets.equivalent(a, b)` | +| `ext.Math()` | Math functions | `math.greatest(a, b)`, `math.least(a, b)` | + +Plus custom Dex libraries in the `pkg/cel/library` subpackage, each implementing the +`cel.Library` interface: + +**`library.Email`** — email-related helpers: + +| Function | Signature | Description | +|----------|-----------|-------------| +| `dex.emailDomain` | `(string) -> string` | Returns the domain portion of an email address. `dex.emailDomain("user@example.com") == "example.com"` | +| `dex.emailLocalPart` | `(string) -> string` | Returns the local part of an email address. `dex.emailLocalPart("user@example.com") == "user"` | + +**`library.Groups`** — group-related helpers: + +| Function | Signature | Description | +|----------|-----------|-------------| +| `dex.groupMatches` | `(list(string), string) -> list(string)` | Returns groups matching a glob pattern. `dex.groupMatches(identity.groups, "team:*")` | +| `dex.groupFilter` | `(list(string), list(string)) -> list(string)` | Returns only groups present in the allowed list. `dex.groupFilter(identity.groups, ["admin", "ops"])` | + +#### Example: Compile and Evaluate + +```go +// 1. Create a compiler with identity and request variables +compiler, _ := cel.NewCompiler( + append(cel.IdentityVariables(), cel.RequestVariables()...), +) + +// 2. Compile a policy expression (type-checked, cost-estimated) +prog, _ := compiler.CompileBool( + `identity.email.endsWith('@example.com') && 'admin' in identity.groups`, +) + +// 3. Evaluate against real data +result, _ := cel.EvalBool(ctx, prog, map[string]any{ + "identity": cel.IdentityFromConnector(connectorIdentity), + "request": cel.RequestFromContext(cel.RequestContext{...}), +}) +// result == true +``` + +### Phase 2: Authentication Policies + +**Config Model:** + +```go +// AuthPolicy is a list of deny expressions evaluated after a user +// authenticates with a connector. Each expression evaluates to bool. +// If true — the request is denied. Evaluated in order; first match wins. +type AuthPolicy []PolicyExpression + +// PolicyExpression is a CEL expression with an optional human-readable message. +type PolicyExpression struct { + // Expression is a CEL expression that evaluates to bool. + Expression string `json:"expression"` + // Message is a CEL expression that evaluates to string (displayed to the user on deny). + // If empty, a generic message is shown. + Message string `json:"message,omitempty"` +} +``` + +**Evaluation point:** After `connector.CallbackConnector.HandleCallback()` or +`connector.PasswordConnector.Login()` returns an identity, and before the auth request is +finalized. Implemented in `server/handlers.go` at `handleConnectorCallback`. + +**Available CEL variables:** `identity` (from connector), `request` (client_id, connector_id, +scopes, redirect_uri). + +**Compilation:** All policy expressions are compiled once at config load time (in +`cmd/dex/serve.go`) and stored in the `Server` struct. This ensures: +- Syntax/type errors are caught at startup, not at runtime. +- No compilation overhead per request. +- Cost estimation can warn operators about expensive expressions at startup. + +**Evaluation flow:** + +``` +User authenticates via connector + │ + v +connector.HandleCallback() returns Identity + │ + v +Evaluate global authPolicy (in order) + - For each expression: evaluate → bool + - If true → deny with message, HTTP 403 + │ + v +Evaluate per-client authPolicy (in order) + - Same logic as global + │ + v +Continue normal flow (approval screen or redirect) +``` + +### Phase 3: Token Policies + +**Config Model:** + +```go +// TokenPolicy defines policies for token issuance. +type TokenPolicy struct { + // Claims adds or overrides claims in the issued ID token. + Claims []ClaimExpression `json:"claims,omitempty"` + // Filter validates the token request. If expression evaluates to false, + // the request is denied. + Filter *PolicyExpression `json:"filter,omitempty"` +} + +type ClaimExpression struct { + // Key is a CEL expression evaluating to string — the claim name. + Key string `json:"key"` + // Value is a CEL expression evaluating to dyn — the claim value. + Value string `json:"value"` + // Condition is an optional CEL expression evaluating to bool. + // When set, the claim is only included in the token if the condition + // evaluates to true. If omitted, the claim is always included. + Condition string `json:"condition,omitempty"` +} +``` + +**Evaluation point:** In `server/oauth2.go` during ID token construction, after standard +claims are built but before JWT signing. + +**Available CEL variables:** `identity`, `request`, `existing_claims` (the standard claims already +computed as `map(string, dyn)`). + +**Claim merge order:** +1. Standard Dex claims (sub, iss, aud, email, groups, etc.) +2. Global `tokenPolicy.claims` evaluated and merged +3. Per-client `tokenPolicy.claims` evaluated and merged (overrides global) + +**Reserved (forbidden) claim names:** + +Certain claim names are reserved and MUST NOT be set or overridden by CEL token policy +expressions. Attempting to use a reserved claim key will result in a config validation error at +startup. This prevents operators from accidentally breaking the OIDC/OAuth2 contract or +undermining Dex's security guarantees. + +```go +// ReservedClaimNames is the set of claim names that CEL token policy +// expressions are forbidden from setting. These are core OIDC/OAuth2 claims +// managed exclusively by Dex. +var ReservedClaimNames = map[string]struct{}{ + "iss": {}, // Issuer — always set by Dex to its own issuer URL + "sub": {}, // Subject — derived from connector identity, must not be spoofed + "aud": {}, // Audience — determined by the OAuth2 client, not policy + "exp": {}, // Expiration — controlled by Dex token TTL configuration + "iat": {}, // Issued At — set by Dex at signing time + "nbf": {}, // Not Before — set by Dex at signing time + "jti": {}, // JWT ID — generated by Dex for token revocation/uniqueness + "auth_time": {}, // Authentication Time — set by Dex from the auth session + "nonce": {}, // Nonce — echoed from the client's authorization request + "at_hash": {}, // Access Token Hash — computed by Dex from the access token + "c_hash": {}, // Code Hash — computed by Dex from the authorization code +} +``` + +The reserved list is enforced in two places: +1. **Config load time** — When compiling token policy `ClaimExpression` entries, Dex statically + evaluates the `Key` expression (which must be a string literal or constant-foldable) and rejects + it if the result is in `ReservedClaimNames`. +2. **Runtime (defense in depth)** — Before merging evaluated claims into the ID token, Dex checks + each key against `ReservedClaimNames` and logs a warning + skips the claim if it matches. This + guards against dynamic key expressions that couldn't be statically checked. + +### Phase 4: OIDC Connector Claim Mapping + +**Config Model:** + +In `connector/oidc/oidc.go`: + +```go +type Config struct { + // ... existing fields ... + + // ClaimMappingExpressions provides CEL-based claim mapping. + // When set, these take precedence over ClaimMapping and ClaimMutations. + ClaimMappingExpressions *ClaimMappingExpression `json:"claimMappingExpressions,omitempty"` +} + +type ClaimMappingExpression struct { + // Username is a CEL expression evaluating to string. + // Available variable: 'claims' (map of upstream claims). + Username string `json:"username,omitempty"` + // Email is a CEL expression evaluating to string. + Email string `json:"email,omitempty"` + // Groups is a CEL expression evaluating to list(string). + Groups string `json:"groups,omitempty"` + // EmailVerified is a CEL expression evaluating to bool. + EmailVerified string `json:"emailVerified,omitempty"` + // Extra is a map of claim names to CEL expressions evaluating to dyn. + // These are carried through to token policies. + Extra map[string]string `json:"extra,omitempty"` +} +``` + +**Available CEL variable:** `claims` — `map(string, dyn)` containing all raw upstream claims from +the ID token and/or UserInfo endpoint. + +This replaces the need for `ClaimMapping`, `NewGroupFromClaims`, `FilterGroupClaims`, and +`ModifyGroupNames` with a single, more powerful mechanism. + +**Backward compatibility:** When `claimMappingExpressions` is nil, the existing `ClaimMapping` and +`ClaimMutations` logic is used unchanged. When `claimMappingExpressions` is set, a startup warning is +logged if legacy mapping fields are also configured. + +### Policy Application Flow + +The following diagram shows the order in which CEL policies are applied. +Each step is optional — if not configured, it is skipped. + +``` +Connector Authentication + │ + │ upstream claims → connector.Identity + │ + v +Authentication Policies + │ + │ Global authPolicy + │ Per-client authPolicy + │ + v +Token Issuance + │ + │ Global tokenPolicy.filter + │ Per-client tokenPolicy.filter + │ + │ Global tokenPolicy.claims + │ Per-client tokenPolicy.claims + │ + │ Sign JWT + │ + v +Token Response +``` + +| Step | Policy | Scope | Action on match | +|------|--------|-------|-----------------| +| 2 | `authPolicy` (global) | Global | Expression → `true` = DENY login | +| 3 | `authPolicy` (per-client) | Per-client | Expression → `true` = DENY login | +| 4 | `tokenPolicy.filter` (global) | Global | Expression → `false` = DENY token | +| 5 | `tokenPolicy.filter` (per-client) | Per-client | Expression → `false` = DENY token | +| 6 | `tokenPolicy.claims` (global) | Global | Adds/overrides claims (with optional condition) | +| 7 | `tokenPolicy.claims` (per-client) | Per-client | Adds/overrides claims (overrides global) | + +### Risks and Mitigations + +| Risk | Mitigation | +|------|------------| +| **CEL expression complexity / DoS** | Cost budgets with configurable limits (default aligned with Kubernetes). Expressions are validated at config load time. Runtime evaluation is aborted if cost exceeds budget. | +| **Learning curve for operators** | CEL has excellent documentation, playground ([cel.dev](https://cel.dev)), and massive CNCF adoption. Dex docs will include a dedicated CEL guide with examples. Most operators already know CEL from Kubernetes. | +| **`cel-go` dependency size** | `cel-go` adds ~5MB to binary. This is acceptable for the functionality provided. Kubernetes, Istio, Envoy all accept this trade-off. | +| **Breaking changes in `cel-go`** | Pin to semver minor range. Environment versioning ensures existing expressions continue to work across upgrades. | +| **Security: CEL expression injection** | CEL expressions are defined by operators in the server config, not by end users. No CEL expression is ever constructed from user input at runtime. | +| **Config migration** | Old config fields (`ClaimMapping`, `ClaimMutations`) continue to work. CEL expressions are opt-in. If both are specified, CEL takes precedence with a config-time warning. | +| **Error messages exposing internals** | CEL deny `message` expressions are controlled by the operator. Default messages are generic. Evaluation errors are logged server-side, not exposed to end users. | +| **Performance** | Expressions are compiled once at startup. Evaluation is sub-millisecond for typical identity operations. Cost budgets prevent pathological cases. Benchmarks will be included in `pkg/cel` tests. | + +### Alternatives + +#### OPA/Rego + +OPA was previously considered ([#1635], token exchange DEP). While powerful, it has significant +drawbacks for Dex: + +- **Separate daemon** — OPA typically runs as a sidecar or daemon; adds operational complexity. + Even the embedded Go library (`github.com/open-policy-agent/opa/rego`) is significantly + heavier than `cel-go`. +- **Rego learning curve** — Rego is a Datalog-derived language unfamiliar to most developers. + CEL syntax is closer to C/Java/Go and is immediately readable. +- **Overkill** — Dex needs simple expression evaluation, not a full policy engine with data + loading, bundles, and partial evaluation. +- **No inline expressions** — Rego policies are typically separate files, not inline config + expressions. This makes the config harder to understand and deploy. +- **Smaller CNCF footprint for embedding** — While OPA is a graduated CNCF project, CEL has + broader adoption as an _embedded_ language (Kubernetes, Istio, Envoy, Kyverno, etc.). + +#### JMESPath + +JMESPath was proposed for claim mapping. Drawbacks: + +- **Query-only** — JMESPath is a JSON query language. It cannot express boolean conditions, + mutations, or string operations naturally. +- **Limited type system** — No type checking at compile time. Errors are only caught at runtime. +- **Small ecosystem** — Limited adoption compared to CEL. No CNCF projects use JMESPath for + policy evaluation. +- **No cost estimation** — No way to bound execution time. + +#### Hardcoded Go Logic + +The current approach: each feature requires new Go structs, config fields, and code. This is +unsustainable: +- `ClaimMapping`, `NewGroupFromClaims`, `FilterGroupClaims`, `ModifyGroupNames` are each separate + features that could be one CEL expression. +- Every new policy need requires a Dex code change and release. +- Combinatorial explosion of config options. + +#### No Change + +Without CEL or an equivalent: +- Operators continue to request per-client connector restrictions, custom claims, claim + transformations, and access policies — issues remain open indefinitely. +- Dex accumulates more ad-hoc config fields, increasing maintenance burden. +- Complex use cases require external reverse proxies, forking Dex, or middleware. + +## Future Improvements + +- **CEL in other connectors** — Extend CEL claim mapping beyond OIDC to LDAP (attribute mapping), + SAML (assertion mapping), and other connectors with complex attribute mapping needs. +- **Policy testing framework** — Unit test framework for operators to validate their CEL + expressions against fixture data before deployment. +- **Connector selection via CEL** — Replace the static connector-per-client mapping with a CEL + expression that dynamically determines which connectors to show based on request attributes. + + diff --git a/docs/enhancements/id-jag-2026-03-02#4600.md b/docs/enhancements/id-jag-2026-03-02#4600.md new file mode 100644 index 0000000000..231921c883 --- /dev/null +++ b/docs/enhancements/id-jag-2026-03-02#4600.md @@ -0,0 +1,283 @@ +# Dex Enhancement Proposal (DEP) 4600 - 2026-03-02 - Identity Assertion JWT Authorization Grant (ID-JAG) + +## Table of Contents + +- [Dex Enhancement Proposal (DEP) 4600 - 2026-03-02 - Identity Assertion JWT Authorization Grant (ID-JAG)](#dex-enhancement-proposal-dep-4600---2026-03-02---identity-assertion-jwt-authorization-grant-id-jag) + - [Table of Contents](#table-of-contents) + - [Summary](#summary) + - [Context](#context) + - [Motivation](#motivation) + - [Goals/Pain](#goalspain) + - [Non-goals](#non-goals) + - [Proposal](#proposal) + - [User Experience](#user-experience) + - [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints) + - [Observability](#observability) + - [Risks and Mitigations](#risks-and-mitigations) + - [Alternatives](#alternatives) + - [Future Improvements](#future-improvements) + +## Summary + +[draft-ietf-oauth-identity-assertion-authz-grant-02] specifies a mechanism +for an application to use an identity assertion to obtain an access token +for a third-party API by coordinating through a common enterprise identity +provider using Token Exchange [RFC 8693] and JWT Profile for OAuth 2.0 +Authorization Grants [RFC 7523]. + +This DEP proposes to extend Dex's existing Token Exchange implementation +to support issuing Identity Assertion JWT Authorization Grants (ID-JAGs), +enabling cross-domain access managed by the enterprise IdP. + +[draft-ietf-oauth-identity-assertion-authz-grant-02]: https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/ + +## Context + +- [#2812 DEP for RFC 8693 OAuth 2 Token Exchange] + established the Token Exchange foundation that ID-JAG builds upon. +- [draft-ietf-oauth-identity-assertion-authz-grant-02] + is the IETF Standards Track specification this DEP implements. +- [draft-ietf-oauth-identity-chaining] + is the broader identity chaining specification that ID-JAG profiles. + +The specification is authored by A. Parecki (Okta), K. McGuinness, and +B. Campbell (Ping Identity). It is actively being developed within the +IETF OAuth Working Group. + +Use cases: + +- LLM agents accessing enterprise APIs on behalf of users (Appendix A.3 of the spec) +- Enterprise applications embedding content from third-party apps +- Email/calendaring applications accessing cross-domain resources + +Real-world adoption: + +- [Okta Cross App Access] is GA, implementing ID-JAG for SaaS-to-SaaS and + AI agent scenarios with a developer tutorial available. +- [Okta AI Agent Token Exchange] (Early Access) uses ID-JAG for AI agents + accessing enterprise APIs on behalf of authenticated users. +- [Keycloak #43971] tracks ID-JAG support as a feature request. +- The upcoming MCP (Model Context Protocol) specification references ID-JAG + for AI agent authorization flows. + +[#2812 DEP for RFC 8693 OAuth 2 Token Exchange]: https://github.com/dexidp/dex/pull/2812 +[draft-ietf-oauth-identity-chaining]: https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-chaining/ +[Okta Cross App Access]: https://developer.okta.com/blog/2026/02/10/xaa-client +[Okta AI Agent Token Exchange]: https://developer.okta.com/docs/guides/ai-agent-token-exchange/authserver/main/ +[Keycloak #43971]: https://github.com/keycloak/keycloak/issues/43971 + +## Motivation + +### Goals/Pain + +In enterprise environments, applications are configured for SSO through +a common IdP. When one application needs to access a user's data at another +application, the current approach requires either: + +1. A direct OAuth flow between apps (bypassing the IdP's visibility and policy) +2. Static API keys or service accounts (security risk) + +ID-JAG solves this by letting the IdP broker cross-domain access, maintaining +visibility and policy control. + +**Specific goals:** + +- Issue ID-JAG tokens via Token Exchange (`requested_token_type=urn:ietf:params:oauth:token-type:id-jag`) +- Support `audience` and `resource` parameters per the specification +- Validate subject token audience against requesting client +- Support configurable policy evaluation for token exchange requests + +### Non-goals + +- Implementing the Resource Authorization Server role (JWT Bearer Grant / RFC 7523) + is out of scope for this initial DEP. It may be addressed in a follow-up. +- SAML assertion support as `subject_token_type` is deferred. +- Step-up authentication flow is deferred. + +## Proposal + +### User Experience + +End-to-end flow: + +```mermaid +sequenceDiagram + participant C as Client (Wiki App) + participant Dex as Dex (IdP AS) + participant RAS as Resource AS (Chat AS) + participant RS as Resource Server (Chat API) + + C->>Dex: 1. OIDC Authentication (authorization_code flow) + Dex-->>C: ID Token + optional Refresh Token + + C->>Dex: 2. Token Exchange (grant_type=token-exchange)
subject_token=ID Token
requested_token_type=id-jag
audience=https://acme.chat.example/
connector_id=google + Note over Dex: Validate subject_token aud == client_id
Evaluate policy (clientID → allowed audiences)
Issue ID-JAG JWT (typ: oauth-id-jag+jwt) + Dex-->>C: ID-JAG (access_token, token_type=N_A, expires_in=300) + + C->>RAS: 3. JWT Bearer Grant (RFC 7523)
grant_type=jwt-bearer, assertion=ID-JAG + Note over RAS: Validate ID-JAG signature (Dex JWKS)
Validate aud == RAS issuer
Issue access token + RAS-->>C: Access Token + + C->>RS: 4. API Request with Access Token + RS-->>C: Protected Resource +``` + +Clients can request ID-JAG tokens from Dex's `/token` endpoint by specifying +`requested_token_type=urn:ietf:params:oauth:token-type:id-jag` in a +Token Exchange request. ID-JAG support is enabled by adding +`urn:ietf:params:oauth:token-type:id-jag` to `oauth2.tokenExchange.tokenTypes`. +When not listed, requests with this `requested_token_type` are rejected, +ensuring no change in behavior for existing deployments. + +The request parameters (extending existing Token Exchange): + +- `grant_type`: REQUIRED - `urn:ietf:params:oauth:grant-type:token-exchange` +- `subject_token`: REQUIRED - the identity assertion (OpenID Connect ID Token) +- `subject_token_type`: REQUIRED - `urn:ietf:params:oauth:token-type:id_token`. + SAML 2.0 (`urn:ietf:params:oauth:token-type:saml2`) is deferred (see Non-goals). +- `requested_token_type`: REQUIRED - `urn:ietf:params:oauth:token-type:id-jag` +- `audience`: REQUIRED - the Issuer URL of the Resource Authorization Server. + **Note**: The existing Token Exchange implementation uses a Dex-specific `connector_id` + parameter (not part of RFC 8693) for connector selection. The `audience` parameter was + not used in the current implementation despite DEP #2812 originally proposing it for + connector identification. ID-JAG introduces `audience` with its standard RFC 8693 + meaning (target Resource AS). This is purely additive and does not affect existing + Token Exchange requests. +- `connector_id`: REQUIRED (Dex extension) - the ID of the Dex connector to verify the + subject token against. The connector validates the token (issuer, signature, etc.), + so a mismatched token is rejected. This parameter already exists in the current + Token Exchange implementation and is reused as-is. +- `resource`: OPTIONAL - the Resource Identifier of the Resource Server +- `scope`: OPTIONAL - the requested scopes at the Resource Server + +The response: + +- `access_token`: the ID-JAG JWT (named `access_token` for RFC 8693 compatibility) +- `issued_token_type`: `urn:ietf:params:oauth:token-type:id-jag` +- `token_type`: `N_A` (this is not an OAuth access token) +- `expires_in`: lifetime in seconds (default: 300, configurable independently of ID token + lifetime via `expiry.idJAGTokens`) +- `scope`: OPTIONAL if the issued scope is identical to the requested scope; REQUIRED + otherwise. Per Section 4.3.2 of the specification, policy evaluation at the IdP may + result in different scopes being issued than were requested. + +Complete configuration example: + +```yaml +oauth2: + grantTypes: + - authorization_code + - urn:ietf:params:oauth:grant-type:token-exchange + tokenExchange: + # List of token types enabled for exchange. Adding id-jag enables ID-JAG support. + # Omitting it (default) disables ID-JAG without affecting other token exchange flows. + # SAML2 (urn:ietf:params:oauth:token-type:saml2) may be added in a future release. + tokenTypes: + - urn:ietf:params:oauth:token-type:id_token + - urn:ietf:params:oauth:token-type:id-jag + +expiry: + idTokens: "24h" + idJAGTokens: "5m" # default: 5m; independent of idTokens + +staticClients: + - id: wiki-app + name: "Wiki Application" + secret: "wiki-secret" + redirectURIs: + - "https://wiki.example/callback" + # Per-client ID-JAG policy. Clients without this section cannot obtain ID-JAG tokens + # (default-deny). Only audiences and scopes listed here may be requested. + idJAGPolicies: + allowedAudiences: + - "https://chat.example/" + - "https://calendar.example/" + allowedScopes: + - "chat.read" + - "calendar.read" + + - id: supermarket-app + name: "Supermarket Application" + secret: "supermarket-secret" + redirectURIs: + - "https://supermarket.example/callback" + idJAGPolicies: + allowedAudiences: + - "https://grocery.store.1/" + - "https://grocery.store.2/" + allowedScopes: + - "eat.bananas" + - "eat.apples" +``` + +### Implementation Details/Notes/Constraints + +- A new `id-jag` branch is added to the existing Token Exchange flow, issuing a signed JWT + per Section 3 of the specification (header `typ: "oauth-id-jag+jwt"`, claims including + `iss`, `sub`, `aud`, `client_id`, `jti`, `exp`, `iat`). + +- Per-client `idJAGPolicies` in `staticClients` control which audiences and scopes a + given client may request in an ID-JAG. Clients without `idJAGPolicies` are denied + by default. Dynamically registered clients are currently unsupported for ID-JAG policies; + support via CEL expressions (building on the CEL infrastructure from #4601) is future work. + +- OIDC discovery is extended with `identity_chaining_requested_token_types_supported` per + Section 7 of the specification. When ID-JAG is enabled, Dex includes + `urn:ietf:params:oauth:token-type:id-jag` in this metadata property. + +- ID-JAG support is enabled by listing `urn:ietf:params:oauth:token-type:id-jag` in + `oauth2.tokenExchange.tokenTypes`. When not listed (default), requests are rejected, + ensuring no change in behavior for existing deployments. + +### Observability + +- Every ID-JAG token exchange request (issued or rejected) emits a structured log entry + with `client_id`, `connector_id`, `audience`, `resource` (if present), requested and + granted `scope` (these may differ after policy evaluation), `sub`, `jti` (if issued), + and the policy decision (`approved`/`denied` with reason like `audience_not_allowed` + or `client_has_no_policy`). + +- The following Prometheus counters are exposed: + - `dex_id_jag_requests_total` (labels: `result`) — issued vs rejected + - `dex_id_jag_policy_rejections_total` (labels: `reason`) — + breakdown by denial reason, useful for spotting misconfigurations or abuse + - `dex_id_jag_scope_modifications_total` — cases where policy reduced the requested scopes + +### Risks and Mitigations + +- **Lateral movement risk**: Same as existing Token Exchange. Mitigated by + not issuing refresh tokens, short expiry (5 min recommended), and + policy-based audience restrictions. +- **Token confusion**: The `typ: "oauth-id-jag+jwt"` header and distinct + `issued_token_type` prevent confusion with ID Tokens or access tokens. +- **Replay attack risk**: Server-side `jti` tracking is deferred, so a stolen ID-JAG + can be replayed within its 5-minute lifetime. Short `expires_in` is the only Dex-side + mitigation; Resource Authorization Servers should implement `jti` caching independently. +- **Public client misuse**: Per Section 8.1 of the specification, ID-JAG SHOULD only be + used by confidential clients. Public clients should use the standard authorization code + flow with interactive user consent at the Resource Authorization Server. Dex will enforce + this by rejecting ID-JAG requests from public clients (clients without a secret). +- **Breaking changes**: None. This is purely additive to the existing + Token Exchange implementation. The `audience` parameter is newly introduced + (not previously used in the implementation despite DEP #2812's original proposal), + and `connector_id` already exists. + +### Alternatives + +- **Wait for spec finalization**: The draft is Standards Track and stable enough + to implement. Okta and Ping Identity (the spec authors) already ship implementations, + and the spec has been adopted by the IETF OAuth WG. +- **External policy engine (OPA/CEL)**: Config-based policies are sufficient for now. + The CEL infrastructure (#4601) is merged; ID-JAG policy evaluation via CEL is future work. + +## Future Improvements + +- Resource Authorization Server role (JWT Bearer Grant / RFC 7523) + accepting ID-JAGs from external IdPs +- SAML 2.0 assertion support as `subject_token_type` + (`urn:ietf:params:oauth:token-type:saml2`) +- CEL-based ID-JAG policy evaluation (building on #4601) enabling dynamic policies for + DB-managed clients, including runtime policy changes without restart +- Step-up authentication when authentication context is insufficient +- `actor_token` support for delegation scenarios +- Server-side `jti` tracking to prevent ID-JAG replay attacks diff --git a/docs/enhancements/token-exchange-2023-02-03-#2812.md b/docs/enhancements/token-exchange-2023-02-03-#2812.md new file mode 100644 index 0000000000..f9f556d26e --- /dev/null +++ b/docs/enhancements/token-exchange-2023-02-03-#2812.md @@ -0,0 +1,175 @@ +# Dex Enhancement Proposal (DEP) 2812 - 2023-02-03 - Token Exchange + +## Table of Contents + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals/Pain](#goals) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) + - [User Experience](#user-experience) + - [Implementation Details/Notes/Constraints](#implementation-detailsnotesconstraints) + - [Risks and Mitigations](#risks-and-mitigations) + - [Alternatives](#alternatives) +- [Future Improvements](#future-improvements) + +## Summary + +[RFC 8693] specifies a new OAuth2 `grant_type` of `urn:ietf:params:oauth:grant-type:token-exchange`. +Using this grant type, when clients start an authentication flow with Dex, +in lieu of being redirected to their upstream IDP for authentication on demand, +clients can present an independently obtained, valid token from their IDP to Dex. +This is primarily useful in fully automated environments with job/machine identities, +where there is no human in the loop to handle browser-based login flows. +This DEP proposes to implement the new grant type for Dex. + +[RFC 8693]: https://www.rfc-editor.org/rfc/rfc8693.html + +## Context + +- [#1668 Question: non-web based clients?] + was closed with no real resolution +- [#1484 Token exchange for external tokens] + mentions that Keycloak has a similar capability +- [#2657 Get OIDC token issued by Dex using a token issued by one of the connectors] + is similar to the previous issue, but this time links to the new (January 2020) [RFC 8693]. + +I believe the context for all of these are similar: +a downstream project using Dex as its only IDP wants to grant access to programmatic clients +without issuing long lived API tokens. + +Examples of downstream issues: + +- [argoproj/argo-cd#11632 ArgoCD SSO login via Azure AD Auth using OIDC not work for cli sso login] + +Other related Dex issues: + +- [#2450 Non-OIDC JWT Connector] is a functionally similar request, but expanded to arbitrary JWTs +- [#1225 GitHub Non-Web application flow support] also asks for an exchange, but for an opaque GitHub PAT + +More broadly, this fits into recent movements to issue machine identities: + +- [GCP Service Identity](https://cloud.google.com/run/docs/securing/service-identity) +- [AWS Execution Role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html) +- [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) +- [CircleCI OIDC](https://circleci.com/docs/openid-connect-tokens/) +- [Kubernetes Service Accounts](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) +- [SPIFFE](https://spiffe.io/) + +and granting access to resources based on trusting federated identities: + +- [GCP Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) +- [AWS STS AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) + +[#1484 Token exchange for external tokens]: https://github.com/dexidp/dex/issues/1484 +[#1668 Question: non-web based clients?]: https://github.com/dexidp/dex/issues/1668 +[#2657 Get OIDC token issued by Dex using a token issued by one of the connectors]: https://github.com/dexidp/dex/issues/2657 +[argoproj/argo-cd#11632 ArgoCD SSO login via Azure AD Auth using OIDC not work for cli sso login]: https://github.com/argoproj/argo-cd/issues/11632 +[#2450 Non-OIDC JWT Connector]: https://github.com/dexidp/dex/issues/2450 +[#1225 GitHub Non-Web application flow support]: https://github.com/dexidp/dex/issues/1225 + +An initial attempt is at [#2806](https://github.com/dexidp/dex/pull/2806) + +## Motivation + +### Goals/Pain + +The goal is to allow programmatic access to Dex-protected resources +without the use of static/long-lived secret tokens (API keys, username/password) +or web-based redirect flows. +Such scenarios are common in CI/CD workflows, +and in general automation of common tasks. + +### Non-goals + +- Work will be scoped to just the OIDC connector +- [RFC 8693 Section 2.1.1. Relationship between Resource, Audience, and Scope] + details more complex authorization checks based on targeted resources. + This is considered out of scope. + +[RFC 8693 Section 2.1.1. Relationship between Resource, Audience, and Scope]: https://www.rfc-editor.org/rfc/rfc8693.html#name-relationship-between-resour + +## Proposal + +### User Experience + +Clients can make `POST` requests with `application/x-www-form-urlencoded` +parameters as specified by [RFC 8693] to Dex's `/token` endpoint. +If successful, an access token will be returned, +allowing direct authentication with Dex. +No refresh tokens will be issued, +perform a new exchange (possibly with refreshed upstream tokens) to obtain a new access token. + +The request parameters from [RFC 8693 Section 2.1](https://www.rfc-editor.org/rfc/rfc8693.html#name-request): + +- `grant_type`: REQUIRED - `urn:ietf:params:oauth:grant-type:token-exchange` +- `resource`: OPTIONAL - the `audience` in the issued Dex token +- `audience`: REQUIRED (RFC OPTIONAL) - the connector to verify the provided token against +- `scope`: OPTIONAL - the `scope` in the issued Dex token +- `requested_token_type`: OPTIONAL - one of `urn:ietf:params:oauth:token-type:access_token` or `urn:ietf:params:oauth:token-type:id_token`, defaulting to access token +- `subject_token`: REQUIRED - the token issued by the upstream IDP +- `subject_token_type`: REQUIRED - `urn:ietf:params:oauth:token-type:id_token` or `urn:ietf:params:oauth:token-type:access_token` if `getUserInfo` is `true`. +- `actor_token`: OPTIONAL - unused +- `actor_token_type`: OPTIONAL - unused + +The response parameters from [RFC 8693 Section 2.2](https://www.rfc-editor.org/rfc/rfc8693.html#name-response): + +- `access_token`: the issued token, the field is called `access_token` for legacy reasons +- `issued_token_type`: the actual type of the issued token +- `token_type`: the value `Bearer` +- `expires_in`: validity lifetime in seconds +- `scope`: the requested scope +- `refresh_token`: unused + +The connector only needs to be configured with an issuer, +no client ID / client secrets are necessary + +```yaml +connectors: +- type: oidc + id: my-platform + name: My Platform + config: + issuer: https://oidc.my-platform.example/ +``` + +We expose a global and connector setting, +`allowedGrantTypes: []string` defaulting to all implemented types. + +### Implementation Details/Notes/Constraints + +- Connectors expose a new interface `TokenIdentity` that will verify the given token and return the associated identity. + A Dex access/id token is then minted for the given identity. + +- `actor_token` and `actor_token_type` are "MUST ... if the actor token is present, + also perform the appropriate validation procedures for its indicated token type". + We will ignore these fields for the initial implementation. + + +### Risks and Mitigations + +With token exchanges (sometimes known as identity impersonation), +is they allow for easier lateral movement if an attacker gains access to an upstream token. +We limit the potential impact by not issuing refresh tokens, preventing persistent access. +Combined with short token lifetimes, it should limit the period of time between authentication to upstream IDPs. +Additionally, a new `allowedGrantTypes` would allow for disabling exchanges if the functionality isn't needed. + +### Alternatives + +- Continue to use static keys - + this is a secret management nightmare + and quite painful when client storage of keys is [breached](https://circleci.com/blog/january-4-2023-security-alert/) + +## Future Improvements + +- Other connectors may wish to implement the same capability under Oauth +- The password connector could be switch to support this new endpoint, submitting passwords as access tokens, + allowing for multiple password connectors to be configured +- The `audience` field could be made optional if there is a single connector or the id token is inspected for issuer url +- The `actor_token` and `actor_token_type` can be checked / validated if a suitable use case is determined. +- A policy language like [cel] or [rego] as mentioned on [#1635 Connector Middleware] + would allow for stronger assertions of the provided identity against requested resource access. + +[cel]: https://github.com/google/cel-go +[rego]: https://www.openpolicyagent.org/docs/latest/policy-language/ +[#1635 Connector Middleware]: https://github.com/dexidp/dex/issues/1635 diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml index bf11570a4e..434c8938c5 100644 --- a/examples/config-dev.yaml +++ b/examples/config-dev.yaml @@ -52,12 +52,23 @@ web: # https: 127.0.0.1:5554 # tlsCert: /etc/dex/tls.crt # tlsKey: /etc/dex/tls.key + # headers: + # X-Frame-Options: "DENY" + # X-Content-Type-Options: "nosniff" + # X-XSS-Protection: "1; mode=block" + # Content-Security-Policy: "default-src 'self'" + # Strict-Transport-Security: "max-age=31536000; includeSubDomains" + # clientRemoteIP: + # header: X-Forwarded-For + # trustedProxies: + # - 10.0.0.0/8 # Configuration for dex appearance # frontend: # issuer: dex # logoURL: theme/logo.png # dir: web/ +# Allowed values: light, dark # theme: light # Configuration for telemetry @@ -84,6 +95,14 @@ telemetry: # validIfNotUsedFor: "2160h" # 90 days # absoluteLifetime: "3960h" # 165 days +# Authentication sessions configuration. +# Requires DEX_SESSIONS_ENABLED=true feature flag. +# sessions: +# cookieName: "dex_session" +# absoluteLifetime: "24h" +# validIfNotUsedFor: "1h" +# rememberMeCheckedByDefault: false + # Options for controlling the logger. # logger: # level: "debug" @@ -91,17 +110,52 @@ telemetry: # Default values shown below # oauth2: - # use ["code", "token", "id_token"] to enable implicit flow for web-only clients +# # grantTypes determines the allowed set of authorization flows. +# grantTypes: +# - "authorization_code" +# - "client_credentials" +# - "refresh_token" +# - "implicit" +# - "password" +# - "urn:ietf:params:oauth:grant-type:device_code" +# - "urn:ietf:params:oauth:grant-type:token-exchange" +# # responseTypes determines the allowed response contents of a successful authorization flow. +# # use ["code", "token", "id_token"] to enable implicit flow for web-only clients. # responseTypes: [ "code" ] # also allowed are "token" and "id_token" - # By default, Dex will ask for approval to share data with application - # (approval for sharing data from connected IdP to Dex is separate process on IdP) +# # By default, Dex will ask for approval to share data with application +# # (approval for sharing data from connected IdP to Dex is separate process on IdP) # skipApprovalScreen: false - # If only one authentication method is enabled, the default behavior is to - # go directly to it. For connected IdPs, this redirects the browser away - # from application to upstream provider such as the Google login page +# # If only one authentication method is enabled, the default behavior is to +# # go directly to it. For connected IdPs, this redirects the browser away +# # from application to upstream provider such as the Google login page # alwaysShowLoginScreen: false - # Uncomment the passwordConnector to use a specific connector for password grants +# # Uncomment the passwordConnector to use a specific connector for password grants # passwordConnector: local +# # PKCE (Proof Key for Code Exchange) configuration +# pkce: +# # If true, PKCE is required for all authorization code flows (OAuth 2.1). +# enforce: false +# # Supported code challenge methods. Defaults to ["S256", "plain"]. +# codeChallengeMethodsSupported: ["S256", "plain"] + +# Multi-factor authentication configuration. +# Requires DEX_SESSIONS_ENABLED=true feature flag. +mfa: + authenticators: + - id: totp-1 + type: TOTP + config: + issuer: "dex-1" +# # Optional: limit this authenticator to specific connector types (e.g., ldap, oidc, saml). +# # If omitted or empty, applies to all connector types. +# # It is recommended to use this option to prevent MFA from being used for connectors +# # with their own MFA mechanisms, e.g., OIDC, Google, etc. (but technically, it is possible). +# connectorTypes: +# - mockCallback +# # Default MFA chain applied to clients that don't specify their own mfaChain. +# # If omitted or empty, no MFA is required by default. +# defaultMFAChain: +# - totp-1 # Instead of reading from an external storage, use this list of clients. # @@ -110,17 +164,49 @@ staticClients: - id: example-app redirectURIs: - 'http://127.0.0.1:5555/callback' + - '/dex/device/callback' name: 'Example App' secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + # Optional: restrict which connectors this client can use for authentication. + # If omitted or empty, all connectors are allowed. + # allowedConnectors: + # - mock + # Optional: ordered list of MFA authenticator IDs the user must complete during login. + # References authenticator IDs from mfa.authenticators. + # If omitted, mfa.defaultMFAChain is used. + # mfaChain: + # - totp-1 + +# Example using environment variables +# Set DEX_CLIENT_ID and DEX_SECURE_CLIENT_SECRET before starting Dex +# - idEnv: DEX_CLIENT_ID +# secretEnv: DEX_CLIENT_SECRET +# redirectURIs: +# - 'http://127.0.0.1:5556/callback' +# name: 'Secure Example App' + # - id: example-device-client # redirectURIs: # - /device/callback # name: 'Static Client for Device Flow' # public: true + connectors: - type: mockCallback id: mock name: Example + # grantTypes restricts which grant types can use this connector. + # If not specified, all grant types are allowed. + # Supported values: + # - "authorization_code" + # - "implicit" + # - "refresh_token" + # - "password" + # - "urn:ietf:params:oauth:grant-type:device_code" + # - "urn:ietf:params:oauth:grant-type:token-exchange" +# grantTypes: +# - "authorization_code" +# - "refresh_token" # - type: google # id: google # name: Google @@ -145,4 +231,24 @@ staticPasswords: # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" + name: "Admin User" + emailVerified: true + preferredUsername: "admin" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + +# Settings for signing JWT tokens. Available options: +# - "local": use local keys (only RSA keys supported) +# - "vault": use Vault Transit backend (RSA and EC keys supported) +signer: + type: local + config: + keysRotationPeriod: "6h" +# signer +# type: vault +# config: +# addr: http://127.0.0.1:8200 +# token: root +# keyName: dex-key diff --git a/examples/example-app/handlers.go b/examples/example-app/handlers.go new file mode 100644 index 0000000000..fce65d77c8 --- /dev/null +++ b/examples/example-app/handlers.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +func (a *app) handleIndex(w http.ResponseWriter, r *http.Request) { + renderIndex(w, indexPageData{ + ScopesSupported: a.scopesSupported, + LogoURI: dexLogoDataURI, + }) +} + +func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, fmt.Sprintf("failed to parse form: %v", err), http.StatusBadRequest) + return + } + + // Only use scopes that are checked in the form + scopes := r.Form["extra_scopes"] + crossClients := r.Form["cross_client"] + + // Build complete scope list with audience scopes + scopes = buildScopes(scopes, crossClients) + + connectorID := "" + if id := r.FormValue("connector_id"); id != "" { + connectorID = id + } + + authCodeURL := "" + + var authCodeOptions []oauth2.AuthCodeOption + + if a.pkce { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("code_challenge", codeChallenge)) + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("code_challenge_method", "S256")) + } + + // Check if offline_access scope is present to determine offline access mode + hasOfflineAccess := false + for _, scope := range scopes { + if scope == "offline_access" { + hasOfflineAccess = true + break + } + } + + if hasOfflineAccess && !a.offlineAsScope { + // Provider uses access_type=offline instead of offline_access scope + authCodeOptions = append(authCodeOptions, oauth2.AccessTypeOffline) + // Remove offline_access from scopes as it's not supported + filteredScopes := make([]string, 0, len(scopes)) + for _, scope := range scopes { + if scope != "offline_access" { + filteredScopes = append(filteredScopes, scope) + } + } + scopes = filteredScopes + } + + authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, authCodeOptions...) + + // Parse the auth code URL and safely add connector_id parameter if provided + u, err := url.Parse(authCodeURL) + if err != nil { + http.Error(w, "Failed to parse auth URL", http.StatusInternalServerError) + return + } + + if connectorID != "" { + query := u.Query() + query.Set("connector_id", connectorID) + u.RawQuery = query.Encode() + } + + http.Redirect(w, r, u.String(), http.StatusSeeOther) +} + +func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) { + var ( + err error + token *oauth2.Token + ) + + ctx := oidc.ClientContext(r.Context(), a.client) + oauth2Config := a.oauth2Config(nil) + switch r.Method { + case http.MethodGet: + // Authorization redirect callback from OAuth2 auth flow. + if errMsg := r.FormValue("error"); errMsg != "" { + http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest) + return + } + code := r.FormValue("code") + if code == "" { + http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest) + return + } + if state := r.FormValue("state"); state != exampleAppState { + http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest) + return + } + + var authCodeOptions []oauth2.AuthCodeOption + if a.pkce { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("code_verifier", codeVerifier)) + } + + token, err = oauth2Config.Exchange(ctx, code, authCodeOptions...) + case http.MethodPost: + // Form request from frontend to refresh a token. + refresh := r.FormValue("refresh_token") + if refresh == "" { + http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest) + return + } + t := &oauth2.Token{ + RefreshToken: refresh, + Expiry: time.Now().Add(-time.Hour), + } + token, err = oauth2Config.TokenSource(ctx, t).Token() + default: + http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest) + return + } + + if err != nil { + http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError) + return + } + + parseAndRenderToken(w, r, a, token) +} diff --git a/examples/example-app/handlers_device.go b/examples/example-app/handlers_device.go new file mode 100644 index 0000000000..40209ca215 --- /dev/null +++ b/examples/example-app/handlers_device.go @@ -0,0 +1,273 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" +) + +func (a *app) handleDeviceLogin(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse request body to get options + var reqBody struct { + Scopes []string `json:"scopes"` + CrossClients []string `json:"cross_clients"` + ConnectorID string `json:"connector_id"` + } + + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + http.Error(w, fmt.Sprintf("failed to parse request body: %v", err), http.StatusBadRequest) + return + } + + // Build complete scope list with audience scopes (same as handleLogin) + scopes := buildScopes(reqBody.Scopes, reqBody.CrossClients) + + // Build scope string + scopeStr := strings.Join(scopes, " ") + + // Get device authorization endpoint + // Properly construct the device code endpoint URL + authURL := a.provider.Endpoint().AuthURL + deviceAuthURL := strings.TrimSuffix(authURL, "/auth") + "/device/code" + + // Request device code + data := url.Values{} + data.Set("client_id", a.clientID) + data.Set("client_secret", a.clientSecret) + data.Set("scope", scopeStr) + + // Add connector_id if specified + if reqBody.ConnectorID != "" { + data.Set("connector_id", reqBody.ConnectorID) + } + + resp, err := a.client.PostForm(deviceAuthURL, data) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to request device code: %v", err), http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body := new(bytes.Buffer) + body.ReadFrom(resp.Body) + http.Error(w, fmt.Sprintf("Device code request failed: %s", body.String()), resp.StatusCode) + return + } + + var deviceResp struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri"` + VerificationURIComplete string `json:"verification_uri_complete"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` + } + + if err := json.NewDecoder(resp.Body).Decode(&deviceResp); err != nil { + http.Error(w, fmt.Sprintf("Failed to decode device response: %v", err), http.StatusInternalServerError) + return + } + + // Store device flow data with new session + sessionID := generateSessionID() + + a.deviceFlowMutex.Lock() + a.deviceFlowData.sessionID = sessionID + a.deviceFlowData.deviceCode = deviceResp.DeviceCode + a.deviceFlowData.userCode = deviceResp.UserCode + a.deviceFlowData.verificationURI = deviceResp.VerificationURI + a.deviceFlowData.pollInterval = deviceResp.Interval + if a.deviceFlowData.pollInterval == 0 { + a.deviceFlowData.pollInterval = 5 + } + a.deviceFlowData.token = nil + a.deviceFlowMutex.Unlock() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "ok", + "session_id": sessionID, + }) +} + +func (a *app) handleDevicePage(w http.ResponseWriter, r *http.Request) { + a.deviceFlowMutex.Lock() + data := devicePageData{ + SessionID: a.deviceFlowData.sessionID, + DeviceCode: a.deviceFlowData.deviceCode, + UserCode: a.deviceFlowData.userCode, + VerificationURI: a.deviceFlowData.verificationURI, + PollInterval: a.deviceFlowData.pollInterval, + LogoURI: dexLogoDataURI, + } + a.deviceFlowMutex.Unlock() + + if data.DeviceCode == "" { + http.Error(w, "No device flow in progress", http.StatusBadRequest) + return + } + + renderDevice(w, data) +} + +func (a *app) handleDevicePoll(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + DeviceCode string `json:"device_code"` + SessionID string `json:"session_id"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + a.deviceFlowMutex.Lock() + storedSessionID := a.deviceFlowData.sessionID + storedDeviceCode := a.deviceFlowData.deviceCode + existingToken := a.deviceFlowData.token + a.deviceFlowMutex.Unlock() + + // Check if this session has been superseded by a new one + if req.SessionID != storedSessionID { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusGone) + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": "session_expired", + "error_description": "This device flow session has been superseded by a new one", + }) + return + } + + if req.DeviceCode != storedDeviceCode { + http.Error(w, "Invalid device code", http.StatusBadRequest) + return + } + + // If we already have a token, return success + if existingToken != nil { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "complete", + }) + return + } + + // Poll the token endpoint + tokenURL := a.provider.Endpoint().TokenURL + + data := url.Values{} + data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + data.Set("device_code", req.DeviceCode) + data.Set("client_id", a.clientID) + data.Set("client_secret", a.clientSecret) + + tokenResp, err := a.client.PostForm(tokenURL, data) + if err != nil { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "pending", + }) + return + } + defer tokenResp.Body.Close() + + if tokenResp.StatusCode == http.StatusOK { + // Success! We got the token + // Parse the full response including id_token + var tokenData struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + IDToken string `json:"id_token"` + } + + if err := json.NewDecoder(tokenResp.Body).Decode(&tokenData); err != nil { + http.Error(w, "Failed to decode token", http.StatusInternalServerError) + return + } + + // Create oauth2.Token with all fields + token := &oauth2.Token{ + AccessToken: tokenData.AccessToken, + TokenType: tokenData.TokenType, + RefreshToken: tokenData.RefreshToken, + } + + // Add id_token to Extra + token = token.WithExtra(map[string]interface{}{ + "id_token": tokenData.IDToken, + }) + + // Store the token + a.deviceFlowMutex.Lock() + a.deviceFlowData.token = token + a.deviceFlowMutex.Unlock() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "complete", + }) + return + } + + // Check for errors + var errorResp struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + } + + if err := json.NewDecoder(tokenResp.Body).Decode(&errorResp); err == nil { + if errorResp.Error == "authorization_pending" { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "pending", + }) + return + } + + // Other errors + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tokenResp.StatusCode) + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": errorResp.Error, + "error_description": errorResp.ErrorDescription, + }) + return + } + + // Unknown response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "pending", + }) +} + +func (a *app) handleDeviceResult(w http.ResponseWriter, r *http.Request) { + a.deviceFlowMutex.Lock() + token := a.deviceFlowData.token + a.deviceFlowMutex.Unlock() + + if token == nil { + http.Error(w, "No token available", http.StatusBadRequest) + return + } + + parseAndRenderToken(w, r, a, token) +} diff --git a/examples/example-app/handlers_userinfo.go b/examples/example-app/handlers_userinfo.go new file mode 100644 index 0000000000..36bab85169 --- /dev/null +++ b/examples/example-app/handlers_userinfo.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +func (a *app) handleUserInfo(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse form to get access token + if err := r.ParseForm(); err != nil { + http.Error(w, fmt.Sprintf("Failed to parse form: %v", err), http.StatusBadRequest) + return + } + + accessToken := r.FormValue("access_token") + if accessToken == "" { + http.Error(w, "access_token is required", http.StatusBadRequest) + return + } + + // Get UserInfo endpoint from provider + userInfoEndpoint := a.provider.Endpoint().AuthURL + if len(userInfoEndpoint) > 5 { + // Replace /auth with /userinfo + userInfoEndpoint = userInfoEndpoint[:len(userInfoEndpoint)-5] + "/userinfo" + } + + // Create request to UserInfo endpoint + req, err := http.NewRequestWithContext(r.Context(), "GET", userInfoEndpoint, nil) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create request: %v", err), http.StatusInternalServerError) + return + } + + // Add Authorization header with access token + req.Header.Set("Authorization", "Bearer "+accessToken) + + // Make the request + resp, err := a.client.Do(req) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to fetch userinfo: %v", err), http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + http.Error(w, fmt.Sprintf("UserInfo request failed: %s", string(body)), resp.StatusCode) + return + } + + // Parse and return the userinfo + var userInfo map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + http.Error(w, fmt.Sprintf("Failed to decode userinfo: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(userInfo) +} diff --git a/examples/example-app/main.go b/examples/example-app/main.go index 451bea5b46..389d2ff04b 100644 --- a/examples/example-app/main.go +++ b/examples/example-app/main.go @@ -1,21 +1,14 @@ package main import ( - "bytes" "context" - "crypto/tls" - "crypto/x509" - "encoding/json" "errors" "fmt" "log" - "net" "net/http" - "net/http/httputil" "net/url" "os" - "strings" - "time" + "sync" "github.com/coreos/go-oidc/v3/oidc" "github.com/spf13/cobra" @@ -24,68 +17,44 @@ import ( const exampleAppState = "I wish to wash my irish wristwatch" +var ( + codeVerifier string + codeChallenge string +) + +func init() { + codeVerifier = oauth2.GenerateVerifier() + codeChallenge = oauth2.S256ChallengeFromVerifier(codeVerifier) +} + type app struct { clientID string clientSecret string + pkce bool redirectURI string - verifier *oidc.IDTokenVerifier - provider *oidc.Provider + verifier *oidc.IDTokenVerifier + provider *oidc.Provider + scopesSupported []string // Does the provider use "offline_access" scope to request a refresh token // or does it use "access_type=offline" (e.g. Google)? offlineAsScope bool client *http.Client -} -// return an HTTP client which trusts the provided root CAs. -func httpClientForRootCAs(rootCAs string) (*http.Client, error) { - tlsConfig := tls.Config{RootCAs: x509.NewCertPool()} - rootCABytes, err := os.ReadFile(rootCAs) - if err != nil { - return nil, fmt.Errorf("failed to read root-ca: %v", err) - } - if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { - return nil, fmt.Errorf("no certs found in root CA file %q", rootCAs) + // Device flow state + // Only one session is possible at a time + // Since it is an example, we don't bother locking', this is a simplicity tradeoff + deviceFlowMutex sync.Mutex + deviceFlowData struct { + sessionID string // Unique ID for current flow session + deviceCode string + userCode string + verificationURI string + pollInterval int + token *oauth2.Token } - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tlsConfig, - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, nil -} - -type debugTransport struct { - t http.RoundTripper -} - -func (d debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { - reqDump, err := httputil.DumpRequest(req, true) - if err != nil { - return nil, err - } - log.Printf("%s", reqDump) - - resp, err := d.t.RoundTrip(req) - if err != nil { - return nil, err - } - - respDump, err := httputil.DumpResponse(resp, true) - if err != nil { - resp.Body.Close() - return nil, err - } - log.Printf("%s", respDump) - return resp, nil } func cmd() *cobra.Command { @@ -174,9 +143,16 @@ func cmd() *cobra.Command { a.provider = provider a.verifier = provider.Verifier(&oidc.Config{ClientID: a.clientID}) + a.scopesSupported = s.ScopesSupported + http.Handle("/static/", http.StripPrefix("/static/", staticHandler)) http.HandleFunc("/", a.handleIndex) http.HandleFunc("/login", a.handleLogin) + http.HandleFunc("/device/login", a.handleDeviceLogin) + http.HandleFunc("/device", a.handleDevicePage) + http.HandleFunc("/device/poll", a.handleDevicePoll) + http.HandleFunc("/device/result", a.handleDeviceResult) + http.HandleFunc("/userinfo", a.handleUserInfo) http.HandleFunc(u.Path, a.handleCallback) switch listenURL.Scheme { @@ -193,6 +169,7 @@ func cmd() *cobra.Command { } c.Flags().StringVar(&a.clientID, "client-id", "example-app", "OAuth2 client ID of this application.") c.Flags().StringVar(&a.clientSecret, "client-secret", "ZXhhbXBsZS1hcHAtc2VjcmV0", "OAuth2 client secret of this application.") + c.Flags().BoolVar(&a.pkce, "pkce", true, "Use PKCE flow for the code exchange.") c.Flags().StringVar(&a.redirectURI, "redirect-uri", "http://127.0.0.1:5555/callback", "Callback URL for OAuth2 responses.") c.Flags().StringVar(&issuerURL, "issuer", "http://127.0.0.1:5556/dex", "URL of the OpenID Connect issuer.") c.Flags().StringVar(&listen, "listen", "http://127.0.0.1:5555", "HTTP(S) address to listen at.") @@ -209,131 +186,3 @@ func main() { os.Exit(2) } } - -func (a *app) handleIndex(w http.ResponseWriter, r *http.Request) { - renderIndex(w) -} - -func (a *app) oauth2Config(scopes []string) *oauth2.Config { - return &oauth2.Config{ - ClientID: a.clientID, - ClientSecret: a.clientSecret, - Endpoint: a.provider.Endpoint(), - Scopes: scopes, - RedirectURL: a.redirectURI, - } -} - -func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) { - var scopes []string - if extraScopes := r.FormValue("extra_scopes"); extraScopes != "" { - scopes = strings.Split(extraScopes, " ") - } - var clients []string - if crossClients := r.FormValue("cross_client"); crossClients != "" { - clients = strings.Split(crossClients, " ") - } - for _, client := range clients { - scopes = append(scopes, "audience:server:client_id:"+client) - } - connectorID := "" - if id := r.FormValue("connector_id"); id != "" { - connectorID = id - } - - authCodeURL := "" - scopes = append(scopes, "openid", "profile", "email") - if r.FormValue("offline_access") != "yes" { - authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState) - } else if a.offlineAsScope { - scopes = append(scopes, "offline_access") - authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState) - } else { - authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, oauth2.AccessTypeOffline) - } - if connectorID != "" { - authCodeURL = authCodeURL + "&connector_id=" + connectorID - } - - http.Redirect(w, r, authCodeURL, http.StatusSeeOther) -} - -func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) { - var ( - err error - token *oauth2.Token - ) - - ctx := oidc.ClientContext(r.Context(), a.client) - oauth2Config := a.oauth2Config(nil) - switch r.Method { - case http.MethodGet: - // Authorization redirect callback from OAuth2 auth flow. - if errMsg := r.FormValue("error"); errMsg != "" { - http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest) - return - } - code := r.FormValue("code") - if code == "" { - http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest) - return - } - if state := r.FormValue("state"); state != exampleAppState { - http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest) - return - } - token, err = oauth2Config.Exchange(ctx, code) - case http.MethodPost: - // Form request from frontend to refresh a token. - refresh := r.FormValue("refresh_token") - if refresh == "" { - http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest) - return - } - t := &oauth2.Token{ - RefreshToken: refresh, - Expiry: time.Now().Add(-time.Hour), - } - token, err = oauth2Config.TokenSource(ctx, t).Token() - default: - http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest) - return - } - - if err != nil { - http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError) - return - } - - rawIDToken, ok := token.Extra("id_token").(string) - if !ok { - http.Error(w, "no id_token in token response", http.StatusInternalServerError) - return - } - - idToken, err := a.verifier.Verify(r.Context(), rawIDToken) - if err != nil { - http.Error(w, fmt.Sprintf("failed to verify ID token: %v", err), http.StatusInternalServerError) - return - } - - accessToken, ok := token.Extra("access_token").(string) - if !ok { - http.Error(w, "no access_token in token response", http.StatusInternalServerError) - return - } - - var claims json.RawMessage - if err := idToken.Claims(&claims); err != nil { - http.Error(w, fmt.Sprintf("error decoding ID token claims: %v", err), http.StatusInternalServerError) - return - } - - buff := new(bytes.Buffer) - if err := json.Indent(buff, []byte(claims), "", " "); err != nil { - http.Error(w, fmt.Sprintf("error indenting ID token claims: %v", err), http.StatusInternalServerError) - return - } - - renderToken(w, a.redirectURI, rawIDToken, accessToken, token.RefreshToken, buff.String()) -} diff --git a/examples/example-app/static/app.js b/examples/example-app/static/app.js new file mode 100644 index 0000000000..14a8a80aec --- /dev/null +++ b/examples/example-app/static/app.js @@ -0,0 +1,154 @@ +(function() { + const crossClientInput = document.getElementById("cross_client_input"); + const crossClientList = document.getElementById("cross-client-list"); + const addClientBtn = document.getElementById("add-cross-client"); + const scopesList = document.getElementById("scopes-list"); + const customScopeInput = document.getElementById("custom_scope_input"); + const addCustomScopeBtn = document.getElementById("add-custom-scope"); + + // Default scopes that should be checked by default + const defaultScopes = ["openid", "profile", "email", "offline_access"]; + + // Check default scopes on page load + document.addEventListener("DOMContentLoaded", function() { + const checkboxes = scopesList.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(cb => { + if (defaultScopes.includes(cb.value)) { + cb.checked = true; + } + }); + }); + + function addCrossClient(value) { + const trimmed = value.trim(); + if (!trimmed) return; + + const chip = document.createElement("div"); + chip.className = "chip"; + + const text = document.createElement("span"); + text.textContent = trimmed; + + const hidden = document.createElement("input"); + hidden.type = "hidden"; + hidden.name = "cross_client"; + hidden.value = trimmed; + + const remove = document.createElement("button"); + remove.type = "button"; + remove.textContent = "×"; + remove.onclick = () => crossClientList.removeChild(chip); + + chip.append(text, hidden, remove); + crossClientList.appendChild(chip); + } + + function addCustomScope(scope) { + const trimmed = scope.trim(); + if (!trimmed || !scopesList) return; + + // Check if scope already exists + const existingCheckboxes = scopesList.querySelectorAll('input[type="checkbox"]'); + for (const cb of existingCheckboxes) { + if (cb.value === trimmed) { + cb.checked = true; + return; + } + } + + // Add new scope checkbox + const scopeItem = document.createElement("div"); + scopeItem.className = "scope-item"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.name = "extra_scopes"; + checkbox.value = trimmed; + checkbox.id = "scope_custom_" + trimmed; + checkbox.checked = true; + + const label = document.createElement("label"); + label.htmlFor = checkbox.id; + label.textContent = trimmed; + + scopeItem.append(checkbox, label); + scopesList.appendChild(scopeItem); + } + + addClientBtn?.addEventListener("click", () => { + addCrossClient(crossClientInput.value); + crossClientInput.value = ""; + crossClientInput.focus(); + }); + + crossClientInput?.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + addCrossClient(crossClientInput.value); + crossClientInput.value = ""; + } + }); + + addCustomScopeBtn?.addEventListener("click", () => { + addCustomScope(customScopeInput.value); + customScopeInput.value = ""; + customScopeInput.focus(); + }); + + customScopeInput?.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + addCustomScope(customScopeInput.value); + customScopeInput.value = ""; + } + }); + + // Device Grant Login Handler + const deviceGrantBtn = document.getElementById("device-grant-btn"); + deviceGrantBtn?.addEventListener("click", async () => { + deviceGrantBtn.disabled = true; + deviceGrantBtn.textContent = "Loading..."; + + try { + // Collect form data similar to regular login + const form = document.getElementById("login-form"); + const formData = new FormData(form); + + // Get selected scopes + const scopes = formData.getAll("extra_scopes"); + + // Get cross-client values + const crossClients = formData.getAll("cross_client"); + + // Get connector_id if specified + const connectorId = formData.get("connector_id") || ""; + + // Initiate device flow with options + const response = await fetch('/device/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + scopes: scopes, + cross_clients: crossClients, + connector_id: connectorId + }) + }); + + if (response.ok) { + // Redirect to device flow page + window.location.href = '/device'; + } else { + const errorText = await response.text(); + alert('Failed to start device flow: ' + errorText); + } + } catch (error) { + alert('Error starting device flow: ' + error.message); + } finally { + deviceGrantBtn.disabled = false; + deviceGrantBtn.textContent = "Device Code Flow"; + } + }); +})(); + diff --git a/examples/example-app/static/device.js b/examples/example-app/static/device.js new file mode 100644 index 0000000000..5896682a16 --- /dev/null +++ b/examples/example-app/static/device.js @@ -0,0 +1,110 @@ +(function() { + const sessionID = document.getElementById("session-id")?.value; + const deviceCode = document.getElementById("device-code")?.value; + const pollInterval = parseInt(document.getElementById("poll-interval")?.value || "5", 10); + const verificationURL = document.getElementById("verification-url")?.textContent; + const userCode = document.getElementById("user-code")?.textContent; + const statusText = document.getElementById("status-text"); + const errorMessage = document.getElementById("error-message"); + const openAuthBtn = document.getElementById("open-auth-btn"); + + let pollTimer = null; + + document.querySelectorAll(".copy-btn").forEach(btn => { + btn.addEventListener("click", async function() { + const targetId = this.getAttribute("data-copy"); + const targetElement = document.getElementById(targetId); + + if (targetElement) { + const textToCopy = targetElement.textContent; + + try { + await navigator.clipboard.writeText(textToCopy); + const originalText = this.textContent; + this.textContent = "✓"; + setTimeout(() => { + this.textContent = originalText; + }, 2000); + } catch (err) { + console.error('Failed to copy:', err); + } + } + }); + }); + + openAuthBtn?.addEventListener("click", () => { + if (verificationURL && userCode) { + const url = verificationURL + "?user_code=" + encodeURIComponent(userCode); + window.open(url, "_blank", "width=600,height=800"); + } + }); + + async function pollForToken() { + try { + const response = await fetch('/device/poll', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_id: sessionID, + device_code: deviceCode + }) + }); + + const data = await response.json(); + + if (response.ok && data.status === 'complete') { + statusText.textContent = "Authentication successful! Redirecting..."; + stopPolling(); + window.location.href = '/device/result'; + } else if (response.ok && data.status === 'pending') { + statusText.textContent = "Waiting for authentication..."; + } else { + const errorText = data.error_description || data.error || 'Unknown error'; + + if (data.error === 'session_expired') { + showError('This session has been superseded by a new device flow. Please start over.'); + stopPolling(); + } else if (data.error === 'expired_token' || data.error === 'access_denied') { + showError(data.error === 'expired_token' ? + 'The device code has expired. Please start over.' : + 'Authentication was denied.'); + stopPolling(); + } + } + } catch (error) { + console.error('Polling error:', error); + } + } + + function showError(message) { + errorMessage.textContent = message; + errorMessage.style.display = 'block'; + + // Hide the status indicator (contains spinner and status text) + const statusIndicator = document.querySelector('.status-indicator'); + if (statusIndicator) { + statusIndicator.style.display = 'none'; + } + } + + function startPolling() { + pollForToken(); + pollTimer = setInterval(pollForToken, pollInterval * 1000); + } + + function stopPolling() { + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + } + + if (deviceCode) { + startPolling(); + } + + window.addEventListener('beforeunload', stopPolling); +})(); + diff --git a/examples/example-app/static/dex-glyph-color.svg b/examples/example-app/static/dex-glyph-color.svg new file mode 100644 index 0000000000..2668039fe9 --- /dev/null +++ b/examples/example-app/static/dex-glyph-color.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/examples/example-app/static/style.css b/examples/example-app/static/style.css new file mode 100644 index 0000000000..fafca567e4 --- /dev/null +++ b/examples/example-app/static/style.css @@ -0,0 +1,589 @@ +body { + font-family: Arial, sans-serif; + background-color: #f2f2f2; + margin: 0; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + flex-direction: column; + padding: 20px; +} + +/* Token page layout - no centering */ +body.token-page { + display: block; + align-items: flex-start; + justify-content: flex-start; + padding: 20px; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 600px; +} + +.logo { + max-width: 100%; + width: 200px; + height: auto; + margin-bottom: 30px; + display: block; +} + +form { + background-color: #fff; + padding: 30px; + border-radius: 8px; + width: 100%; +} + +/* Shadow only for main login form */ +.container form { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.primary-action { + margin-bottom: 20px; +} + +.advanced { + margin-top: 20px; + background: #f9fbfd; + border: 1px solid #dbe7f3; + border-radius: 6px; + padding: 15px; +} + +.advanced summary { + cursor: pointer; + font-weight: bold; + color: #3F9FD8; + user-select: none; + text-align: center; +} + +.advanced summary:hover { + color: #357FAA; +} + +.app-description { + text-align: center; + color: #666; + font-size: 14px; + margin-bottom: 25px; + line-height: 1.5; +} + +.app-description a { + color: #3F9FD8; + text-decoration: none; +} + +.app-description a:hover { + text-decoration: underline; +} + +.field { + margin-top: 15px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.field label { + font-weight: 600; + color: #333; + font-size: 14px; +} + +.inline-input { + display: flex; + gap: 8px; +} + +.inline-input input[type="text"] { + flex: 1; +} + +input[type="text"] { + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +input[type="text"]:focus { + border-color: #3F9FD8; +} + +.checkbox-field { + flex-direction: row; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +.checkbox-field label { + margin: 0; + font-weight: 500; +} + +input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.scopes-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 200px; + overflow-y: auto; + padding: 10px; + background: white; + border: 1px solid #ddd; + border-radius: 4px; +} + +.scope-item { + display: flex; + align-items: center; + gap: 8px; +} + +.scope-item input[type="checkbox"] { + margin: 0; +} + +.scope-item label { + margin: 0; + font-weight: 400; + font-size: 13px; + cursor: pointer; +} + +input[type="submit"], button { + padding: 12px 20px; + background-color: #3F9FD8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.2s; +} + +/* Full width submit button only on main login form */ +form input[type="submit"] { + width: 100%; + font-size: 16px; +} + +/* Token page forms should have auto-width buttons */ +body.token-page input[type="submit"] { + width: auto; + font-size: 14px; +} + +input[type="submit"]:hover, button:hover { + background-color: #357FAA; +} + +button { + white-space: nowrap; +} + +.chip-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-height: 32px; + margin-top: 8px; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + background: #e9f4fb; + border: 1px solid #cde5f5; + border-radius: 16px; + padding: 6px 12px; + font-size: 13px; +} + +.chip button { + border: none; + background: transparent; + cursor: pointer; + font-weight: bold; + color: #3F9FD8; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.chip button:hover { + background: #d0e8f5; +} + +.hint { + font-size: 12px; + color: #666; + margin-top: 4px; +} + +.copy-btn { + padding: 4px 10px; + background-color: #3F9FD8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 11px; + transition: background-color 0.2s; + margin-left: 8px; + white-space: nowrap; +} + +.copy-btn:hover { + background-color: #357FAA; +} + +/* Token page styles */ +.back-button { + display: inline-block; + padding: 8px 16px; + background-color: #EF4B5C; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: fixed; + right: 20px; + bottom: 20px; +} + +.back-button:hover { + background-color: #C43B4B; +} + +.token-block { + background-color: #fff; + padding: 10px 15px; + border-radius: 8px; + margin-bottom: 15px; + word-wrap: break-word; + display: flex; + flex-direction: column; + gap: 5px; + position: relative; + overflow: hidden; + border: 1px solid #e0e0e0; +} + +.token-block form { + margin: 10px 0 0 0; + padding: 0; + background-color: transparent; + box-shadow: none; + border-radius: 0; +} + +.token-title { + font-weight: bold; + display: flex; + justify-content: space-between; + align-items: center; +} + +.token-title a { + font-size: 0.9em; + text-decoration: none; + color: #3F9FD8; +} + +.token-title a:hover { + text-decoration: underline; +} + +.token-code { + overflow-wrap: break-word; + word-break: break-all; + white-space: normal; +} + +pre { + white-space: pre-wrap; + background-color: #f9f9f9; + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; + margin: 0; + font-family: 'Courier New', Courier, monospace; + overflow-x: auto; + font-size: 0.9em; + position: relative; + margin-top: 5px; +} + +pre .key { + color: #c00; +} + +pre .string { + color: #080; +} + +pre .number { + color: #00f; +} + +/* Login Buttons Styles */ +.login-buttons { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; +} + +.login-button { + width: 100%; + padding: 14px 24px; + font-size: 16px; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 600; + border: 2px solid #3F9FD8; +} + +.login-button.primary { + background-color: #3F9FD8; + color: #fff; +} + +.login-button.primary:hover { + background-color: #357FAA; + border-color: #357FAA; +} + +.login-button.secondary { + background-color: #fff; + color: #3F9FD8; +} + +.login-button.secondary:hover { + background-color: #f0f8ff; +} + +/* Device Flow Page Styles */ +.device-flow-container { + background-color: #fff; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + width: 100%; +} + +.device-instructions h2 { + margin-top: 0; + color: #333; + text-align: center; +} + +.instruction-text { + text-align: center; + color: #666; + margin-bottom: 25px; +} + +.verification-info { + display: flex; + flex-direction: column; + gap: 20px; + margin-bottom: 25px; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 8px; +} + +.info-item label { + font-weight: 600; + color: #333; + font-size: 14px; +} + +.code-display { + display: flex; + align-items: center; + gap: 10px; + background-color: #f5f5f5; + padding: 12px; + border-radius: 4px; + border: 1px solid #ddd; +} + +.code-display.large { + padding: 20px; +} + +.code-display code { + flex: 1; + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + word-break: break-all; +} + +.code-display code.user-code { + font-size: 24px; + font-weight: bold; + letter-spacing: 2px; + color: #3F9FD8; +} + +.copy-btn { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + padding: 5px 10px; + border-radius: 4px; + transition: background-color 0.2s; +} + +.copy-btn:hover { + background-color: #e0e0e0; +} + +.copy-btn:active { + background-color: #d0d0d0; +} + +.actions { + text-align: center; +} + +.primary-button { + padding: 12px 32px; + font-size: 16px; + background-color: #3F9FD8; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 600; + transition: background-color 0.3s; +} + +.primary-button:hover { + background-color: #357FAA; +} + +.polling-status { + margin-top: 30px; + padding-top: 30px; + border-top: 1px solid #eee; +} + +.status-indicator { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + color: #666; +} + +.spinner { + width: 20px; + height: 20px; + border: 3px solid #f3f3f3; + border-top: 3px solid #3F9FD8; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-message { + margin-top: 15px; + padding: 12px; + background-color: #fee; + border: 1px solid #fcc; + border-radius: 4px; + color: #c00; + text-align: center; +} + +.device-data { + display: none; +} + +/* UserInfo Styles */ +#userinfo-section { + margin-top: 10px; +} + +.fetch-userinfo-btn { + padding: 10px 20px; + background-color: #3F9FD8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.2s; +} + +.fetch-userinfo-btn:hover { + background-color: #357FAA; +} + +.userinfo-loading { + display: flex; + align-items: center; + gap: 10px; + color: #666; + margin-top: 10px; +} + +.userinfo-loading .spinner { + width: 16px; + height: 16px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3F9FD8; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +#userinfo-claims { + margin-top: 15px; +} + +#userinfo-error { + margin-top: 10px; +} + diff --git a/examples/example-app/static/token.js b/examples/example-app/static/token.js new file mode 100644 index 0000000000..6d410a8559 --- /dev/null +++ b/examples/example-app/static/token.js @@ -0,0 +1,152 @@ +// Simple JSON syntax highlighter +document.addEventListener("DOMContentLoaded", function() { + const claimsElement = document.getElementById("claims"); + if (claimsElement) { + try { + const json = JSON.parse(claimsElement.textContent); + claimsElement.innerHTML = syntaxHighlight(json); + } catch (e) { + console.error("Invalid JSON in claims:", e); + } + } +}); + +function syntaxHighlight(json) { + if (typeof json != 'string') { + json = JSON.stringify(json, undefined, 2); + } + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g, function (match) { + let cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return '' + match + ''; + }); +} + +function copyPublicKey() { + const publicKeyElement = document.getElementById("public-key"); + if (!publicKeyElement) return; + + const text = publicKeyElement.textContent; + + // Use modern clipboard API if available + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(() => { + showCopyFeedback("Copied!"); + }).catch(err => { + console.error("Failed to copy:", err); + fallbackCopy(text); + }); + } else { + fallbackCopy(text); + } +} + +function fallbackCopy(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); + showCopyFeedback("Copied!"); + } catch (err) { + console.error("Fallback copy failed:", err); + showCopyFeedback("Failed to copy"); + } + document.body.removeChild(textarea); +} + +function showCopyFeedback(message) { + const btn = event.target; + const originalText = btn.textContent; + btn.textContent = message; + btn.style.backgroundColor = "#28a745"; + setTimeout(() => { + btn.textContent = originalText; + btn.style.backgroundColor = ""; + }, 2000); +} + +// UserInfo functionality +document.addEventListener("DOMContentLoaded", function() { + const form = document.getElementById("userinfo-form"); + if (form) { + form.addEventListener("submit", fetchUserInfo); + } +}); + +async function fetchUserInfo(event) { + event.preventDefault(); + + const form = event.target; + const loading = document.getElementById("userinfo-loading"); + const error = document.getElementById("userinfo-error"); + const claimsElement = document.getElementById("userinfo-claims"); + const submitButton = form.querySelector('button[type="submit"]'); + + // Hide error and claims from previous attempts + error.style.display = "none"; + claimsElement.style.display = "none"; + + // Show loading, hide button + submitButton.style.display = "none"; + loading.style.display = "flex"; + + try { + const formData = new FormData(form); + + // Convert FormData to URL-encoded string + const urlEncodedData = new URLSearchParams(formData).toString(); + + const response = await fetch("/userinfo", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: urlEncodedData + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || `HTTP ${response.status}`); + } + + const userinfo = await response.json(); + + // Display the userinfo claims + const code = claimsElement.querySelector("code"); + const formattedJson = JSON.stringify(userinfo, null, 2); + code.textContent = formattedJson; + + // Apply syntax highlighting + try { + code.innerHTML = syntaxHighlight(userinfo); + } catch (e) { + console.error("Failed to highlight JSON:", e); + } + + claimsElement.style.display = "block"; + + } catch (err) { + console.error("Failed to fetch userinfo:", err); + error.textContent = "Failed to fetch UserInfo: " + err.message; + error.style.display = "block"; + submitButton.style.display = "inline-block"; + } finally { + loading.style.display = "none"; + } +} + diff --git a/examples/example-app/templates.go b/examples/example-app/templates.go index a9425ead27..b3ae739627 100644 --- a/examples/example-app/templates.go +++ b/examples/example-app/templates.go @@ -1,93 +1,177 @@ package main import ( + "context" + "crypto/rsa" + "crypto/x509" + "embed" + "encoding/base64" + "encoding/json" + "encoding/pem" "html/template" + "io/fs" "log" + "math/big" "net/http" + "net/url" + + "github.com/coreos/go-oidc/v3/oidc" +) + +//go:embed templates/*.html +var templatesFS embed.FS + +//go:embed static/* +var staticFS embed.FS + +const dexLogoDataURI = "/static/dex-glyph-color.svg" + +var ( + indexTmpl *template.Template + tokenTmpl *template.Template + deviceTmpl *template.Template + staticHandler http.Handler ) -var indexTmpl = template.Must(template.New("index.html").Parse(` - - - - -
-

- - -

-

- - -

-

- - -

-

- - -

-

- -

-
- -`)) - -func renderIndex(w http.ResponseWriter) { - renderTemplate(w, indexTmpl, nil) +func init() { + var err error + indexTmpl, err = template.ParseFS(templatesFS, "templates/index.html") + if err != nil { + log.Fatalf("failed to parse index template: %v", err) + } + + tokenTmpl, err = template.ParseFS(templatesFS, "templates/token.html") + if err != nil { + log.Fatalf("failed to parse token template: %v", err) + } + + deviceTmpl, err = template.ParseFS(templatesFS, "templates/device.html") + if err != nil { + log.Fatalf("failed to parse device template: %v", err) + } + + // Create handler for static files + staticSubFS, err := fs.Sub(staticFS, "static") + if err != nil { + log.Fatalf("failed to create static sub filesystem: %v", err) + } + staticHandler = http.FileServer(http.FS(staticSubFS)) +} + +func renderIndex(w http.ResponseWriter, data indexPageData) { + renderTemplate(w, indexTmpl, data) +} + +func renderDevice(w http.ResponseWriter, data devicePageData) { + renderTemplate(w, deviceTmpl, data) +} + +type indexPageData struct { + ScopesSupported []string + LogoURI string +} + +type devicePageData struct { + SessionID string + DeviceCode string + UserCode string + VerificationURI string + PollInterval int + LogoURI string } type tokenTmplData struct { - IDToken string - AccessToken string - RefreshToken string - RedirectURL string - Claims string + IDToken string + IDTokenJWTLink string + AccessToken string + AccessTokenJWTLink string + RefreshToken string + RedirectURL string + Claims string + PublicKeyPEM string } -var tokenTmpl = template.Must(template.New("token.html").Parse(` - - - - -

ID Token:

{{ .IDToken }}

-

Access Token:

{{ .AccessToken }}

-

Claims:

{{ .Claims }}

- {{ if .RefreshToken }} -

Refresh Token:

{{ .RefreshToken }}

-
- - -
- {{ end }} - - -`)) - -func renderToken(w http.ResponseWriter, redirectURL, idToken, accessToken, refreshToken, claims string) { - renderTemplate(w, tokenTmpl, tokenTmplData{ - IDToken: idToken, - AccessToken: accessToken, - RefreshToken: refreshToken, - RedirectURL: redirectURL, - Claims: claims, + +func getPublicKeyPEM(provider *oidc.Provider) string { + if provider == nil { + return "" + } + + jwksURL := provider.Endpoint().AuthURL + if len(jwksURL) > 5 { + jwksURL = jwksURL[:len(jwksURL)-5] + "/keys" + } else { + return "" + } + + resp, err := http.Get(jwksURL) + if err != nil { + return "" + } + defer resp.Body.Close() + + var jwks struct { + Keys []json.RawMessage `json:"keys"` + } + if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil || len(jwks.Keys) == 0 { + return "" + } + + var key struct { + N string `json:"n"` + E string `json:"e"` + Kty string `json:"kty"` + } + if err := json.Unmarshal(jwks.Keys[0], &key); err != nil || key.Kty != "RSA" { + return "" + } + + nBytes, err1 := base64.RawURLEncoding.DecodeString(key.N) + eBytes, err2 := base64.RawURLEncoding.DecodeString(key.E) + if err1 != nil || err2 != nil { + return "" + } + + var eInt int + for _, b := range eBytes { + eInt = eInt<<8 | int(b) + } + + pubKey := &rsa.PublicKey{ + N: new(big.Int).SetBytes(nBytes), + E: eInt, + } + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return "" + } + + pubKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, }) + + return string(pubKeyPEM) +} + +func renderToken(w http.ResponseWriter, ctx context.Context, provider *oidc.Provider, redirectURL, idToken, accessToken, refreshToken, claims string) { + data := tokenTmplData{ + IDToken: idToken, + IDTokenJWTLink: generateJWTIOLink(idToken, provider, ctx), + AccessToken: accessToken, + AccessTokenJWTLink: generateJWTIOLink(accessToken, provider, ctx), + RefreshToken: refreshToken, + RedirectURL: redirectURL, + Claims: claims, + PublicKeyPEM: getPublicKeyPEM(provider), + } + renderTemplate(w, tokenTmpl, data) } func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interface{}) { @@ -98,13 +182,9 @@ func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interfa switch err := err.(type) { case *template.Error: - // An ExecError guarantees that Execute has not written to the underlying reader. log.Printf("Error rendering template %s: %s", tmpl.Name(), err) - - // TODO(ericchiang): replace with better internal server error. http.Error(w, "Internal server error", http.StatusInternalServerError) default: - // An error with the underlying write, such as the connection being - // dropped. Ignore for now. + // An error with the underlying write, such as the connection being dropped. Ignore for now. } } diff --git a/examples/example-app/templates/device.html b/examples/example-app/templates/device.html new file mode 100644 index 0000000000..092ec371d8 --- /dev/null +++ b/examples/example-app/templates/device.html @@ -0,0 +1,61 @@ + + + + + + Device Login - Example App + + + +
+ +
+
+

Device Login

+

Please authenticate on your device:

+ +
+
+ +
+ {{.VerificationURI}} + +
+
+ +
+ +
+ {{.UserCode}} + +
+
+
+ +
+ +
+
+ +
+
+
+ Waiting for authentication... +
+ +
+ + +
+
+ + + + + diff --git a/examples/example-app/templates/index.html b/examples/example-app/templates/index.html new file mode 100644 index 0000000000..063b154fb1 --- /dev/null +++ b/examples/example-app/templates/index.html @@ -0,0 +1,67 @@ + + + + + + Example App - Login + + + +
+ +
+
+ This is an example application for Dex OpenID Connect provider.
+ Learn more in the documentation. +
+ +
+ Advanced options +
+ + Select OpenID Connect scopes to request. Standard scopes are pre-selected. +
+ {{range .ScopesSupported}} +
+ + +
+ {{end}} +
+ {{if eq (len .ScopesSupported) 0}} +
No scopes from discovery - add custom scopes below.
+ {{end}} +
+ + +
+
+
+ + Each client is sent as audience:server:client_id scope. +
+
+ + +
+
+
+ + Specify a connector ID to bypass the connector selection screen. + +
+
+
+
+ + + + + diff --git a/examples/example-app/templates/token.html b/examples/example-app/templates/token.html new file mode 100644 index 0000000000..b003deeb22 --- /dev/null +++ b/examples/example-app/templates/token.html @@ -0,0 +1,84 @@ + + + + + + Tokens + + + + {{ if .IDToken }} +
+
+ ID Token: + Decode on jwt.io +
+
{{ .IDToken }}
+
+ {{ end }} + + {{ if .AccessToken }} +
+
+ Access Token: + Decode on jwt.io +
+
{{ .AccessToken }}
+
+ {{ end }} + + {{ if .Claims }} +
+
ID Token Claims:
+
{{ .Claims }}
+
+ {{ end }} + + {{ if .AccessToken }} +
+
UserInfo:
+
+
+ + +
+ + + +
+
+ {{ end }} + + {{ if .RefreshToken }} +
+
Refresh Token:
+
{{ .RefreshToken }}
+
+ + +
+
+ {{ end }} + + {{ if .PublicKeyPEM }} +
+
+ Public Key (for JWT verification): + +
+
{{ .PublicKeyPEM }}
+
Copy this key and paste it into jwt.io's "Verify Signature" section to validate the token signature.
+
+ {{ end }} + + Back to Home + + + + + diff --git a/examples/example-app/utils.go b/examples/example-app/utils.go new file mode 100644 index 0000000000..099c106284 --- /dev/null +++ b/examples/example-app/utils.go @@ -0,0 +1,154 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "net/http/httputil" + "os" + "slices" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +// generateSessionID creates a random session identifier +func generateSessionID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + // Fallback to timestamp if random fails + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + return hex.EncodeToString(b) +} + +// buildScopes constructs a scope list from base scopes and cross-client IDs +func buildScopes(baseScopes []string, crossClients []string) []string { + scopes := make([]string, len(baseScopes)) + copy(scopes, baseScopes) + + // Add audience scopes for cross-client authorization + for _, client := range crossClients { + if client != "" { + scopes = append(scopes, "audience:server:client_id:"+client) + } + } + + return uniqueStrings(scopes) +} + +func (a *app) oauth2Config(scopes []string) *oauth2.Config { + return &oauth2.Config{ + ClientID: a.clientID, + ClientSecret: a.clientSecret, + Endpoint: a.provider.Endpoint(), + Scopes: scopes, + RedirectURL: a.redirectURI, + } +} + +func uniqueStrings(values []string) []string { + slices.Sort(values) + values = slices.Compact(values) + return values +} + +// return an HTTP client which trusts the provided root CAs. +func httpClientForRootCAs(rootCAs string) (*http.Client, error) { + tlsConfig := tls.Config{RootCAs: x509.NewCertPool()} + rootCABytes, err := os.ReadFile(rootCAs) + if err != nil { + return nil, fmt.Errorf("failed to read root-ca: %v", err) + } + if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { + return nil, fmt.Errorf("no certs found in root CA file %q", rootCAs) + } + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, nil +} + +type debugTransport struct { + t http.RoundTripper +} + +func (d debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { + reqDump, err := httputil.DumpRequest(req, true) + if err != nil { + return nil, err + } + log.Printf("%s", reqDump) + + resp, err := d.t.RoundTrip(req) + if err != nil { + return nil, err + } + + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + resp.Body.Close() + return nil, err + } + log.Printf("%s", respDump) + return resp, nil +} + +func encodeToken(idToken *oidc.IDToken) (string, error) { + var claims json.RawMessage + if err := idToken.Claims(&claims); err != nil { + return "", fmt.Errorf("error decoding ID token claims: %v", err) + } + + buff := new(bytes.Buffer) + if err := json.Indent(buff, claims, "", " "); err != nil { + return "", fmt.Errorf("error indenting ID token claims: %v", err) + } + return buff.String(), nil +} + +func parseAndRenderToken(w http.ResponseWriter, r *http.Request, a *app, token *oauth2.Token) { + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + http.Error(w, "no id_token in token response", http.StatusInternalServerError) + return + } + + idToken, err := a.verifier.Verify(r.Context(), rawIDToken) + if err != nil { + http.Error(w, fmt.Sprintf("failed to verify ID token: %v", err), http.StatusInternalServerError) + return + } + + accessToken, ok := token.Extra("access_token").(string) + if !ok { + accessToken = token.AccessToken + if accessToken == "" { + http.Error(w, "no access_token in token response", http.StatusInternalServerError) + return + } + } + + buf, err := encodeToken(idToken) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + renderToken(w, r.Context(), a.provider, a.redirectURI, rawIDToken, accessToken, token.RefreshToken, buf) +} diff --git a/examples/go.mod b/examples/go.mod index d66c118a7f..3ca1c1a14c 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,25 +1,22 @@ module github.com/dexidp/dex/examples -go 1.17 +go 1.25.0 require ( - github.com/coreos/go-oidc/v3 v3.1.0 - github.com/dexidp/dex/api/v2 v2.0.0 - github.com/spf13/cobra v1.3.0 - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 - google.golang.org/grpc v1.43.0 + github.com/coreos/go-oidc/v3 v3.17.0 + github.com/dexidp/dex/api/v2 v2.4.0 + github.com/spf13/cobra v1.10.2 + golang.org/x/oauth2 v0.36.0 + google.golang.org/grpc v1.79.3 ) require ( - github.com/golang/protobuf v1.5.2 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect - golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect - golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 7907afde92..a92e0dbcce 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,788 +1,56 @@ -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.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/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/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-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -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-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -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-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw= -github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -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/dexidp/dex/api/v2 v2.0.0 h1:bvge1sRmzVzWPWp4WlMzS04lcNQA+jFzHqKV3066bRw= -github.com/dexidp/dex/api/v2 v2.0.0/go.mod h1:k5arBJT1QYvpsEY3sEd0NXJp3hKWKuUUfzJ3BlcqPdM= -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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -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-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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/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/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.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/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.1/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.4/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.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -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/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/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-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -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.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -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/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.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/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -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/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/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.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/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/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/dexidp/dex/api/v2 v2.4.0 h1:gNba7n6BKVp8X4Jp24cxYn5rIIGhM6kDOXcZoL6tr9A= +github.com/dexidp/dex/api/v2 v2.4.0/go.mod h1:/p550ADvFFh7K95VmhUD+jgm15VdaNnab9td8DHOpyI= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= -github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -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/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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI= -golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -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/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -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-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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -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-20201031054903-ff519b6c9102/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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= -golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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-20181026203630-95b1ffbd15a5/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -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-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-20190524140312-2c0ae7006135/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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/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.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0 h1:aCsSLXylHWFno0r4S3joLpiaWayvqd2Mn4iSvx4WZZc= -google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/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.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -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.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= -google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/examples/grpc-client/README.md b/examples/grpc-client/README.md index 59629e0590..6a78df9199 100644 --- a/examples/grpc-client/README.md +++ b/examples/grpc-client/README.md @@ -50,6 +50,9 @@ Running the gRPC client will cause the following API calls to be made to the ser 2. ListPasswords 3. VerifyPassword 4. DeletePassword +5. CreateClient +6. ListClients +7. DeleteClient ## Cleaning up diff --git a/examples/grpc-client/client.go b/examples/grpc-client/client.go index fb8d4aaf06..7bbbac6494 100644 --- a/examples/grpc-client/client.go +++ b/examples/grpc-client/client.go @@ -58,7 +58,7 @@ func createPassword(cli api.DexClient) error { // Create password. if resp, err := cli.CreatePassword(context.TODO(), createReq); err != nil || resp.AlreadyExists { - if resp != nil && resp.AlreadyExists { + if resp != nil && resp.AlreadyExists { return fmt.Errorf("Password %s already exists", createReq.Password.Email) } return fmt.Errorf("failed to create password: %v", err) @@ -125,6 +125,57 @@ func createPassword(cli api.DexClient) error { return nil } +func createAndListClients(cli api.DexClient) error { + client := &api.Client{ + Id: "example-client", + Secret: "example-secret", + RedirectUris: []string{"http://localhost:8080/callback"}, + TrustedPeers: []string{}, + Public: false, + Name: "Example Client", + LogoUrl: "http://example.com/logo.png", + } + + createReq := &api.CreateClientReq{ + Client: client, + } + + if resp, err := cli.CreateClient(context.TODO(), createReq); err != nil || resp.AlreadyExists { + if resp != nil && resp.AlreadyExists { + log.Printf("Client %s already exists", createReq.Client.Id) + } else { + return fmt.Errorf("failed to create client: %v", err) + } + } else { + log.Printf("Created client with ID %s", createReq.Client.Id) + } + + listResp, err := cli.ListClients(context.TODO(), &api.ListClientReq{}) + if err != nil { + return fmt.Errorf("failed to list clients: %v", err) + } + + log.Print("Listing Clients:\n") + for _, client := range listResp.Clients { + log.Printf("ID: %s, Name: %s, Public: %t, RedirectURIs: %v", + client.Id, client.Name, client.Public, client.RedirectUris) + } + + deleteReq := &api.DeleteClientReq{ + Id: client.Id, + } + + if resp, err := cli.DeleteClient(context.TODO(), deleteReq); err != nil || resp.NotFound { + if resp != nil && resp.NotFound { + return fmt.Errorf("Client %s not found", deleteReq.Id) + } + return fmt.Errorf("failed to delete client: %v", err) + } + log.Printf("Deleted client with ID %s", deleteReq.Id) + + return nil +} + func main() { caCrt := flag.String("ca-crt", "", "CA certificate") clientCrt := flag.String("client-crt", "", "Client certificate") @@ -143,4 +194,8 @@ func main() { if err := createPassword(client); err != nil { log.Fatalf("testPassword failed: %v", err) } + + if err := createAndListClients(client); err != nil { + log.Fatalf("testClients failed: %v", err) + } } diff --git a/examples/k8s/dex.yaml b/examples/k8s/dex.yaml index 89ac40b223..c20d268774 100644 --- a/examples/k8s/dex.yaml +++ b/examples/k8s/dex.yaml @@ -23,7 +23,7 @@ spec: spec: serviceAccountName: dex # This is created below containers: - - image: ghcr.io/dexidp/dex:v2.30.0 + - image: ghcr.io/dexidp/dex:v2.32.0 name: dex command: ["/usr/local/bin/dex", "serve", "/etc/dex/cfg/config.yaml"] @@ -106,6 +106,12 @@ data: # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" + name: "Admin User" + emailVerified: true + preferredUsername: "admin" + groups: + - "team-a" + - "team-a/admins" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" --- apiVersion: v1 diff --git a/examples/oidc-conformance/config.yaml.tmpl b/examples/oidc-conformance/config.yaml.tmpl new file mode 100644 index 0000000000..1f89afc64b --- /dev/null +++ b/examples/oidc-conformance/config.yaml.tmpl @@ -0,0 +1,36 @@ +# Dex configuration for OIDC Conformance Testing. +# See https://dexidp.io/docs/development/oidc-certification/ +# +# This template is processed by run.sh which replaces ISSUER_URL and ALIAS +# with actual values before starting Dex. + +issuer: ISSUER_URL/dex + +storage: + type: sqlite3 + config: + file: examples/oidc-conformance/dex.db + +web: + http: 0.0.0.0:5556 + +enablePasswordDB: true + +staticPasswords: +- email: "admin@example.com" + # bcrypt hash of the string "password" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + +staticClients: + - id: first_client + secret: 89d6205220381728e85c4cf5 + redirectURIs: + - https://www.certification.openid.net/test/a/ALIAS/callback + name: First client + + - id: second_client + secret: 51c612288018fd384b05d6ad + redirectURIs: + - https://www.certification.openid.net/test/a/ALIAS/callback + name: Second client diff --git a/examples/oidc-conformance/run.sh b/examples/oidc-conformance/run.sh new file mode 100755 index 0000000000..702928bd18 --- /dev/null +++ b/examples/oidc-conformance/run.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# +# OIDC Conformance Test Runner +# +# Starts Dex with a test configuration and exposes it via a public tunnel +# for use with https://www.certification.openid.net/ +# +# Usage: +# ./run.sh # uses cloudflared (default) +# ./run.sh --tunnel ngrok # uses ngrok +# ./run.sh --url https://my.url # uses a pre-existing public URL (no tunnel) +# ./run.sh --alias my-dex # custom alias for the test plan (default: dex) +# +# Prerequisites: +# - Dex binary in PATH or ../../bin/dex +# - ngrok or cloudflared installed (unless --url is provided) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +DEX_PORT=5556 +TUNNEL_TYPE="cloudflared" +PUBLIC_URL="" +ALIAS="dex" + +while [[ $# -gt 0 ]]; do + case $1 in + --tunnel) TUNNEL_TYPE="$2"; shift 2 ;; + --url) PUBLIC_URL="$2"; shift 2 ;; + --alias) ALIAS="$2"; shift 2 ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +# Find dex binary. +DEX_BIN="" +for candidate in "dex" "$ROOT_DIR/bin/dex"; do + if command -v "$candidate" &>/dev/null || [[ -x "$candidate" ]]; then + DEX_BIN="$candidate" + break + fi +done +if [[ -z "$DEX_BIN" ]]; then + echo "Error: dex binary not found. Run 'make build' first or install dex." + exit 1 +fi + +cleanup() { + echo "" + echo "Shutting down..." + kill "${TUNNEL_PID:-}" "${DEX_PID:-}" 2>/dev/null || true + rm -f "${CONFIG_FILE:-}" + wait 2>/dev/null +} +trap cleanup EXIT + +# Start tunnel if no URL provided. +TUNNEL_PID="" +if [[ -z "$PUBLIC_URL" ]]; then + case "$TUNNEL_TYPE" in + ngrok) + if ! command -v ngrok &>/dev/null; then + echo "Error: ngrok not found. Install it from https://ngrok.com/ or use --url." + exit 1 + fi + ngrok http "$DEX_PORT" --log=stdout --log-level=warn &>/dev/null & + TUNNEL_PID=$! + echo "Waiting for ngrok tunnel..." + sleep 3 + PUBLIC_URL=$(curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"https://[^"]*' | head -1 | cut -d'"' -f4) + if [[ -z "$PUBLIC_URL" ]]; then + echo "Error: failed to get ngrok public URL. Is ngrok running?" + exit 1 + fi + ;; + cloudflared) + if ! command -v cloudflared &>/dev/null; then + echo "Error: cloudflared not found. Install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" + exit 1 + fi + CLOUDFLARED_LOG=$(mktemp) + cloudflared tunnel --url "http://localhost:$DEX_PORT" --no-autoupdate 2>"$CLOUDFLARED_LOG" & + TUNNEL_PID=$! + echo "Waiting for cloudflared tunnel..." + for _ in $(seq 1 30); do + PUBLIC_URL=$(grep -o 'https://[^ ]*\.trycloudflare\.com' "$CLOUDFLARED_LOG" | head -1) && break + sleep 1 + done + rm -f "$CLOUDFLARED_LOG" + if [[ -z "$PUBLIC_URL" ]]; then + echo "Error: failed to get cloudflared URL." + exit 1 + fi + ;; + *) + echo "Error: unknown tunnel type '$TUNNEL_TYPE'. Use 'ngrok' or 'cloudflared'." + exit 1 + ;; + esac +fi + +PUBLIC_URL="${PUBLIC_URL%/}" +echo "Public URL: $PUBLIC_URL" + +# Generate config from template. +CONFIG_FILE=$(mktemp) +sed -e "s|ISSUER_URL|$PUBLIC_URL|g" -e "s|ALIAS|$ALIAS|g" "$SCRIPT_DIR/config.yaml.tmpl" > "$CONFIG_FILE" + +echo "Starting Dex on port $DEX_PORT..." +"$DEX_BIN" serve "$CONFIG_FILE" & +DEX_PID=$! +sleep 2 + +DISCOVERY_URL="$PUBLIC_URL/dex/.well-known/openid-configuration" + +echo "" +echo "============================================================" +echo " OIDC Conformance Test Setup Ready" +echo "============================================================" +echo "" +echo " Discovery URL: $DISCOVERY_URL" +echo " Alias: $ALIAS" +echo "" +echo " Client 1: id=first_client secret=89d6205220381728e85c4cf5" +echo " Client 2: id=second_client secret=51c612288018fd384b05d6ad" +echo "" +echo " Steps:" +echo " 1. Open https://www.certification.openid.net/" +echo " 2. Log in with Google or GitLab" +echo " 3. Create a new test plan:" +echo " - Plan: OpenID Connect Core: Basic Certification Profile" +echo " - Server metadata: discovery" +echo " - Client registration: static_client" +echo " - Alias: $ALIAS" +echo " - Discovery URL: $DISCOVERY_URL" +echo " - Enter both client credentials above" +echo " 4. Run tests and follow instructions" +echo "" +echo " Press Ctrl+C to stop." +echo "============================================================" + +wait "$DEX_PID" diff --git a/flake.lock b/flake.lock index b67b61d98b..302d0832f1 100644 --- a/flake.lock +++ b/flake.lock @@ -1,39 +1,252 @@ { "nodes": { - "flake-utils": { + "cachix": { + "inputs": { + "devenv": [ + "devenv" + ], + "flake-compat": [ + "devenv" + ], + "git-hooks": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748883665, + "narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=", + "owner": "cachix", + "repo": "cachix", + "rev": "f707778d902af4d62d8dd92c269f8e70de09acbe", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "latest", + "repo": "cachix", + "type": "github" + } + }, + "devenv": { + "inputs": { + "cachix": "cachix", + "flake-compat": "flake-compat", + "git-hooks": "git-hooks", + "nix": "nix", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1755355634, + "narHash": "sha256-3UNeb5pBLHtTyYIkzF/3+2YlAKf6OuWQYUQO+qmInA4=", + "owner": "cachix", + "repo": "devenv", + "rev": "85e78cbe26467a2c23c9d34869235740132d749f", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1754487366, + "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "lastModified": 1750779888, + "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "flake-parts": "flake-parts", + "git-hooks-nix": [ + "devenv", + "git-hooks" + ], + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "nixpkgs-23-11": [ + "devenv" + ], + "nixpkgs-regression": [ + "devenv" + ] + }, + "locked": { + "lastModified": 1755029779, + "narHash": "sha256-3+GHIYGg4U9XKUN4rg473frIVNn8YD06bjwxKS1IPrU=", + "owner": "cachix", + "repo": "nix", + "rev": "b0972b0eee6726081d10b1199f54de6d2917f861", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "devenv-2.30", + "repo": "nix", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1662019588, - "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", + "lastModified": 1750441195, + "narHash": "sha256-yke+pm+MdgRb6c0dPt8MgDhv7fcBbdjmv1ZceNTyzKg=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "0ceffe312871b443929ff3006960d29b120dc627", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1753579242, + "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1755268003, + "narHash": "sha256-nNaeJjo861wFR0tjHDyCnHs1rbRtrMgxAKMoig9Sj/w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2da64a81275b68fdad38af669afeda43d401e94b", + "rev": "32f313e49e42f715491e1ea7b306a87c16fe0388", "type": "github" }, "original": { - "id": "nixpkgs", - "ref": "nixos-unstable", - "type": "indirect" + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" } }, "root": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "devenv": "devenv", + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs_2" } } }, diff --git a/flake.nix b/flake.nix index 155ebf99e3..214bf2337f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,56 @@ { - description = "OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors"; - inputs = { - nixpkgs.url = "nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + devenv.url = "github:cachix/devenv"; }; - outputs = { self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - buildDeps = with pkgs; [ git go_1_19 gnumake ]; - devDeps = with pkgs; - buildDeps ++ [ - golangci-lint - gotestsum - protobuf - protoc-gen-go - protoc-gen-go-grpc - kind - ]; - in - { devShell = pkgs.mkShell { buildInputs = devDeps; }; } - ); + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + inputs.devenv.flakeModule + ]; + + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-darwin" + "aarch64-linux" + ]; + + perSystem = + { pkgs, ... }: + rec { + devenv.shells = { + default = { + languages = { + go = { + enable = true; + package = pkgs.go_1_25; + }; + }; + + packages = with pkgs; [ + gnumake + + # golangci-lint + (golangci-lint.override (o: { + buildGoModule = pkgs.buildGo125Module; + })) + gotestsum + protobuf + protoc-gen-go + protoc-gen-go-grpc + kind + ]; + + # https://github.com/cachix/devenv/issues/528#issuecomment-1556108767 + containers = pkgs.lib.mkForce { }; + }; + + ci = devenv.shells.default; + }; + }; + }; } diff --git a/go.mod b/go.mod index d15823aa1b..9d706aaaf5 100644 --- a/go.mod +++ b/go.mod @@ -1,95 +1,134 @@ module github.com/dexidp/dex -go 1.19 +go 1.25.0 require ( - entgo.io/ent v0.11.2 - github.com/AppsFlyer/go-sundheit v0.5.0 + cloud.google.com/go/compute/metadata v0.9.0 + entgo.io/ent v0.14.5 + github.com/AppsFlyer/go-sundheit v0.6.0 github.com/Masterminds/semver v1.5.0 - github.com/Masterminds/sprig/v3 v3.2.2 - github.com/beevik/etree v1.1.0 - github.com/coreos/go-oidc/v3 v3.3.0 - github.com/dexidp/dex/api/v2 v2.1.0 - github.com/felixge/httpsnoop v1.0.3 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/beevik/etree v1.6.0 + github.com/coreos/go-oidc/v3 v3.17.0 + github.com/dexidp/dex/api/v2 v2.4.0 + github.com/fsnotify/fsnotify v1.9.0 github.com/ghodss/yaml v1.0.0 - github.com/go-ldap/ldap/v3 v3.4.4 - github.com/go-sql-driver/mysql v1.6.0 - github.com/gorilla/handlers v1.5.1 - github.com/gorilla/mux v1.8.0 + github.com/go-jose/go-jose/v4 v4.1.3 + github.com/go-ldap/ldap/v3 v3.4.13 + github.com/go-sql-driver/mysql v1.9.3 + github.com/google/cel-go v0.27.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/kylelemons/godebug v1.1.0 - github.com/lib/pq v1.10.5 + github.com/lib/pq v1.12.0 github.com/mattermost/xml-roundtrip-validator v0.1.0 - github.com/mattn/go-sqlite3 v1.14.15 - github.com/oklog/run v1.1.0 + github.com/mattn/go-sqlite3 v1.14.37 + github.com/oklog/run v1.2.0 + github.com/openbao/openbao/api/v2 v2.5.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.13.0 - github.com/russellhaering/goxmldsig v1.2.0 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.5.0 - github.com/stretchr/testify v1.8.0 - go.etcd.io/etcd/client/pkg/v3 v3.5.4 - go.etcd.io/etcd/client/v3 v3.5.4 - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d - golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b - golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 - google.golang.org/api v0.94.0 - google.golang.org/grpc v1.49.0 - google.golang.org/protobuf v1.28.1 - gopkg.in/square/go-jose.v2 v2.6.0 + github.com/pquerna/otp v1.5.0 + github.com/prometheus/client_golang v1.23.2 + github.com/russellhaering/goxmldsig v1.6.0 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 + go.etcd.io/etcd/client/pkg/v3 v3.6.8 + go.etcd.io/etcd/client/v3 v3.6.8 + golang.org/x/crypto v0.49.0 + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 + golang.org/x/net v0.52.0 + golang.org/x/oauth2 v0.36.0 + google.golang.org/api v0.272.0 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.11 ) require ( - ariga.io/atlas v0.5.1-0.20220717122844-8593d7eb1a8e // indirect - cloud.google.com/go/compute v1.7.0 // indirect - github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect + ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.1 // indirect + github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/agext/levenshtein v1.2.1 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/coreos/go-semver v0.3.0 // indirect - github.com/coreos/go-systemd/v22 v22.3.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect - github.com/googleapis/gax-go/v2 v2.4.0 // indirect - github.com/hashicorp/hcl/v2 v2.10.0 // indirect - github.com/huandu/xstrings v1.3.1 // indirect - github.com/imdario/mergo v0.3.11 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/jonboulle/clockwork v0.2.2 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mitchellh/copystructure v1.0.0 // indirect - github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect - github.com/mitchellh/reflectwalk v1.0.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/shopspring/decimal v1.2.0 // indirect - github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/zclconf/go-cty v1.8.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.4 // indirect - go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.17.0 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/hcl/v2 v2.18.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + go.etcd.io/etcd/api/v3 v3.6.8 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/dexidp/dex/api/v2 => ./api/v2 + +tool entgo.io/ent/cmd/ent diff --git a/go.sum b/go.sum index 27fdbb8eea..3bad638c01 100644 --- a/go.sum +++ b/go.sum @@ -1,923 +1,350 @@ -ariga.io/atlas v0.5.1-0.20220717122844-8593d7eb1a8e h1:/r1xGMwmLg4LZ2V3/wWui9TtM3+STh1fp5ExSVRNFZo= -ariga.io/atlas v0.5.1-0.20220717122844-8593d7eb1a8e/go.mod h1:ofVetkJqlaWle3mvYmaS2uyFGFcc7dSq436tmxa/Mzk= -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.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto= -entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU= -github.com/AppsFlyer/go-sundheit v0.5.0 h1:/VxpyigCfJrq1r97mn9HPiAB2qrhcTFHwNIIDr15CZM= -github.com/AppsFlyer/go-sundheit v0.5.0/go.mod h1:2ZM0BnfqT/mljBQO224VbL5XH06TgWuQ6Cn+cTtCpTY= -github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= -github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -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= +ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc= +ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4= +entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AppsFlyer/go-sundheit v0.6.0 h1:d2hBvCjBSb2lUsEWGfPigr4MCOt04sxB+Rppl0yUMSk= +github.com/AppsFlyer/go-sundheit v0.6.0/go.mod h1:LDdBHD6tQBtmHsdW+i1GwdTt6Wqc0qazf5ZEJVTbTME= +github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= +github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= -github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= -github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= -github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -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/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= +github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -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-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -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-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-oidc/v3 v3.3.0 h1:Y1LV3mP+QT3MEycATZpAiwfyN+uxZLqVbAHJUuOJEe4= -github.com/coreos/go-oidc/v3 v3.3.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= +github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= -github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= -github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= -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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= +github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= -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/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= -github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/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.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/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.1/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.4/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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -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/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/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.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= +github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg= -github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= -github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo= +github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= -github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= -github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/openbao/openbao/api/v2 v2.5.1 h1:Br79D6L20SbAa5P7xqENxmvv8LyI4HoKosPy7klhn4o= +github.com/openbao/openbao/api/v2 v2.5.1/go.mod h1:Dh5un77tqGgMbmlVEqjqN+8/dMyUohnkaQVg/wXW0Ig= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/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.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russellhaering/goxmldsig v1.2.0 h1:Y6GTTc9Un5hCxSzVz4UIWQ/zuVwDvzJk80guqzwx6Vg= -github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks= +github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= -github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= -github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= -go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= -go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM= +go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q= +go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50= +go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw= +go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY= +go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -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/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/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 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180811021610-c39426892332/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-20181114220301-adae6a3d119a/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= 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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -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-20201031054903-ff519b6c9102/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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -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/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= 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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/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-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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-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-20190502175342-a43fa875dd82/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/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-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= 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-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-20190524140312-2c0ae7006135/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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -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/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.94.0 h1:KtKM9ru3nzQioV1HLlUf1cR7vMYJIpgls5VhAYQXIwA= -google.golang.org/api v0.94.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/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.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f h1:hJ/Y5SqPXbarffmAsApliUlcvMU+wScNGfyop4bZm8o= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/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.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -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.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/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.0-20210107192922-496545a6307b/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-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= -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= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/pkg/cel/cel.go b/pkg/cel/cel.go new file mode 100644 index 0000000000..8dd686ba72 --- /dev/null +++ b/pkg/cel/cel.go @@ -0,0 +1,232 @@ +package cel + +import ( + "context" + "fmt" + "reflect" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/ext" + + "github.com/dexidp/dex/pkg/cel/library" +) + +// EnvironmentVersion represents the version of the CEL environment. +// New variables, functions, or libraries are introduced in new versions. +type EnvironmentVersion uint32 + +const ( + // EnvironmentV1 is the initial CEL environment. + EnvironmentV1 EnvironmentVersion = 1 +) + +// CompilationResult holds a compiled CEL program ready for evaluation. +type CompilationResult struct { + Program cel.Program + OutputType *cel.Type + Expression string + + ast *cel.Ast +} + +// CompilerOption configures a Compiler. +type CompilerOption func(*compilerConfig) + +type compilerConfig struct { + costBudget uint64 + version EnvironmentVersion +} + +func defaultCompilerConfig() *compilerConfig { + return &compilerConfig{ + costBudget: DefaultCostBudget, + version: EnvironmentV1, + } +} + +// WithCostBudget sets a custom cost budget for expression evaluation. +func WithCostBudget(budget uint64) CompilerOption { + return func(cfg *compilerConfig) { + cfg.costBudget = budget + } +} + +// WithVersion sets the target environment version for the compiler. +// Defaults to the latest version. Specifying an older version ensures +// that only functions/types available at that version are used. +func WithVersion(v EnvironmentVersion) CompilerOption { + return func(cfg *compilerConfig) { + cfg.version = v + } +} + +// Compiler compiles CEL expressions against a specific environment. +type Compiler struct { + env *cel.Env + cfg *compilerConfig +} + +// NewCompiler creates a new CEL compiler with the specified variable +// declarations and options. +// +// All custom Dex libraries are automatically included. +// The environment is configured with cost limits and safe defaults. +func NewCompiler(variables []VariableDeclaration, opts ...CompilerOption) (*Compiler, error) { + cfg := defaultCompilerConfig() + for _, opt := range opts { + opt(cfg) + } + + envOpts := make([]cel.EnvOption, 0, 8+len(variables)) + envOpts = append(envOpts, + cel.DefaultUTCTimeZone(true), + + // Standard extension libraries (same set as Kubernetes) + ext.Strings(), + ext.Encoders(), + ext.Lists(), + ext.Sets(), + ext.Math(), + + // Native Go types for typed variable access. + // This gives compile-time field checking: identity.emial → error at config load. + ext.NativeTypes( + ext.ParseStructTags(true), + reflect.TypeOf(IdentityVal{}), + reflect.TypeOf(RequestVal{}), + ), + + // Custom Dex libraries + cel.Lib(&library.Email{}), + cel.Lib(&library.Groups{}), + + // Presence tests like has(field) and 'key' in map are O(1) hash + // lookups on map(string, dyn) variables, so they should not count + // toward the cost budget. Without this, expressions with multiple + // 'in' checks (e.g. "'admin' in identity.groups") would accumulate + // inflated cost estimates. This matches Kubernetes CEL behavior + // where presence tests are free for CRD validation rules. + cel.CostEstimatorOptions( + checker.PresenceTestHasCost(false), + ), + ) + + for _, v := range variables { + envOpts = append(envOpts, cel.Variable(v.Name, v.Type)) + } + + env, err := cel.NewEnv(envOpts...) + if err != nil { + return nil, fmt.Errorf("failed to create CEL environment: %w", err) + } + + return &Compiler{env: env, cfg: cfg}, nil +} + +// CompileBool compiles a CEL expression that must evaluate to bool. +func (c *Compiler) CompileBool(expression string) (*CompilationResult, error) { + return c.compile(expression, cel.BoolType) +} + +// CompileString compiles a CEL expression that must evaluate to string. +func (c *Compiler) CompileString(expression string) (*CompilationResult, error) { + return c.compile(expression, cel.StringType) +} + +// CompileStringList compiles a CEL expression that must evaluate to list(string). +func (c *Compiler) CompileStringList(expression string) (*CompilationResult, error) { + return c.compile(expression, cel.ListType(cel.StringType)) +} + +// Compile compiles a CEL expression with any output type. +func (c *Compiler) Compile(expression string) (*CompilationResult, error) { + return c.compile(expression, nil) +} + +func (c *Compiler) compile(expression string, expectedType *cel.Type) (*CompilationResult, error) { + if len(expression) > MaxExpressionLength { + return nil, fmt.Errorf("expression exceeds maximum length of %d characters", MaxExpressionLength) + } + + ast, issues := c.env.Compile(expression) + if issues != nil && issues.Err() != nil { + return nil, fmt.Errorf("CEL compilation failed: %w", issues.Err()) + } + + if expectedType != nil && !ast.OutputType().IsEquivalentType(expectedType) { + return nil, fmt.Errorf( + "expected expression output type %s, got %s", + expectedType, ast.OutputType(), + ) + } + + // Estimate cost at compile time and reject expressions that are too expensive. + costEst, err := c.env.EstimateCost(ast, &defaultCostEstimator{}) + if err != nil { + return nil, fmt.Errorf("CEL cost estimation failed: %w", err) + } + + if costEst.Max > c.cfg.costBudget { + return nil, fmt.Errorf( + "CEL expression estimated cost %d exceeds budget %d", + costEst.Max, c.cfg.costBudget, + ) + } + + prog, err := c.env.Program(ast, + cel.EvalOptions(cel.OptOptimize), + cel.CostLimit(c.cfg.costBudget), + ) + if err != nil { + return nil, fmt.Errorf("CEL program creation failed: %w", err) + } + + return &CompilationResult{ + Program: prog, + OutputType: ast.OutputType(), + Expression: expression, + ast: ast, + }, nil +} + +// Eval evaluates a compiled program against the given variables. +func Eval(ctx context.Context, result *CompilationResult, variables map[string]any) (ref.Val, error) { + out, _, err := result.Program.ContextEval(ctx, variables) + if err != nil { + return nil, fmt.Errorf("CEL evaluation failed: %w", err) + } + + return out, nil +} + +// EvalBool is a convenience function that evaluates and asserts bool output. +func EvalBool(ctx context.Context, result *CompilationResult, variables map[string]any) (bool, error) { + out, err := Eval(ctx, result, variables) + if err != nil { + return false, err + } + + v, ok := out.Value().(bool) + if !ok { + return false, fmt.Errorf("expected bool result, got %T", out.Value()) + } + + return v, nil +} + +// EvalString is a convenience function that evaluates and asserts string output. +func EvalString(ctx context.Context, result *CompilationResult, variables map[string]any) (string, error) { + out, err := Eval(ctx, result, variables) + if err != nil { + return "", err + } + + v, ok := out.Value().(string) + if !ok { + return "", fmt.Errorf("expected string result, got %T", out.Value()) + } + + return v, nil +} diff --git a/pkg/cel/cel_test.go b/pkg/cel/cel_test.go new file mode 100644 index 0000000000..b211f344b4 --- /dev/null +++ b/pkg/cel/cel_test.go @@ -0,0 +1,280 @@ +package cel_test + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dexidp/dex/connector" + dexcel "github.com/dexidp/dex/pkg/cel" +) + +func TestCompileBool(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + tests := map[string]struct { + expr string + wantErr bool + }{ + "true literal": { + expr: "true", + }, + "comparison": { + expr: "1 == 1", + }, + "string type mismatch": { + expr: "'hello'", + wantErr: true, + }, + "int type mismatch": { + expr: "42", + wantErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result, err := compiler.CompileBool(tc.expr) + if tc.wantErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + } + }) + } +} + +func TestCompileString(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + tests := map[string]struct { + expr string + wantErr bool + }{ + "string literal": { + expr: "'hello'", + }, + "string concatenation": { + expr: "'hello' + ' ' + 'world'", + }, + "bool type mismatch": { + expr: "true", + wantErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result, err := compiler.CompileString(tc.expr) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + } + }) + } +} + +func TestCompileStringList(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + result, err := compiler.CompileStringList("['a', 'b', 'c']") + assert.NoError(t, err) + assert.NotNil(t, result) + + _, err = compiler.CompileStringList("'not a list'") + assert.Error(t, err) +} + +func TestCompile(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + // Compile accepts any type + result, err := compiler.Compile("true") + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = compiler.Compile("'hello'") + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = compiler.Compile("42") + assert.NoError(t, err) + assert.NotNil(t, result) +} + +func TestCompileErrors(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + tests := map[string]struct { + expr string + }{ + "syntax error": { + expr: "1 +", + }, + "undefined variable": { + expr: "undefined_var", + }, + "undefined function": { + expr: "undefinedFunc()", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + _, err := compiler.Compile(tc.expr) + assert.Error(t, err) + }) + } +} + +func TestCompileRejectsUnknownFields(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + // Typo in field name: should fail at compile time with ObjectType + _, err = compiler.CompileBool("identity.emial == 'test@example.com'") + assert.Error(t, err) + assert.Contains(t, err.Error(), "compilation failed") + + // Type mismatch: comparing string field to int should fail at compile time + _, err = compiler.CompileBool("identity.email == 123") + assert.Error(t, err) + assert.Contains(t, err.Error(), "compilation failed") + + // Valid field: should compile fine + _, err = compiler.CompileBool("identity.email == 'test@example.com'") + assert.NoError(t, err) +} + +func TestMaxExpressionLength(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + longExpr := "'" + strings.Repeat("a", dexcel.MaxExpressionLength) + "'" + _, err = compiler.Compile(longExpr) + assert.Error(t, err) + assert.Contains(t, err.Error(), "maximum length") +} + +func TestEvalBool(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + tests := map[string]struct { + expr string + identity dexcel.IdentityVal + want bool + }{ + "email endsWith": { + expr: "identity.email.endsWith('@example.com')", + identity: dexcel.IdentityVal{Email: "user@example.com"}, + want: true, + }, + "email endsWith false": { + expr: "identity.email.endsWith('@example.com')", + identity: dexcel.IdentityVal{Email: "user@other.com"}, + want: false, + }, + "email_verified": { + expr: "identity.email_verified == true", + identity: dexcel.IdentityVal{EmailVerified: true}, + want: true, + }, + "group membership": { + expr: "identity.groups.exists(g, g == 'admin')", + identity: dexcel.IdentityVal{Groups: []string{"admin", "dev"}}, + want: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prog, err := compiler.CompileBool(tc.expr) + require.NoError(t, err) + + result, err := dexcel.EvalBool(context.Background(), prog, map[string]any{ + "identity": tc.identity, + }) + require.NoError(t, err) + assert.Equal(t, tc.want, result) + }) + } +} + +func TestEvalString(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + // With ObjectType, identity.email is typed as string, so CompileString works. + prog, err := compiler.CompileString("identity.email") + require.NoError(t, err) + + result, err := dexcel.EvalString(context.Background(), prog, map[string]any{ + "identity": dexcel.IdentityVal{Email: "user@example.com"}, + }) + require.NoError(t, err) + assert.Equal(t, "user@example.com", result) +} + +func TestEvalWithIdentityAndRequest(t *testing.T) { + vars := append(dexcel.IdentityVariables(), dexcel.RequestVariables()...) + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + prog, err := compiler.CompileBool( + `identity.email.endsWith('@example.com') && 'admin' in identity.groups && request.connector_id == 'okta'`, + ) + require.NoError(t, err) + + identity := dexcel.IdentityFromConnector(connector.Identity{ + UserID: "123", + Username: "john", + Email: "john@example.com", + Groups: []string{"admin", "dev"}, + }) + request := dexcel.RequestFromContext(dexcel.RequestContext{ + ClientID: "my-app", + ConnectorID: "okta", + Scopes: []string{"openid", "email"}, + }) + + result, err := dexcel.EvalBool(context.Background(), prog, map[string]any{ + "identity": identity, + "request": request, + }) + require.NoError(t, err) + assert.True(t, result) +} + +func TestNewCompilerWithVariables(t *testing.T) { + // Claims variable — remains map(string, dyn) + compiler, err := dexcel.NewCompiler(dexcel.ClaimsVariable()) + require.NoError(t, err) + + // claims.email returns dyn from map access, use Compile (not CompileString) + prog, err := compiler.Compile("claims.email") + require.NoError(t, err) + + result, err := dexcel.EvalString(context.Background(), prog, map[string]any{ + "claims": map[string]any{ + "email": "test@example.com", + }, + }) + require.NoError(t, err) + assert.Equal(t, "test@example.com", result) +} diff --git a/pkg/cel/cost.go b/pkg/cel/cost.go new file mode 100644 index 0000000000..d7a09102b1 --- /dev/null +++ b/pkg/cel/cost.go @@ -0,0 +1,105 @@ +package cel + +import ( + "fmt" + + "github.com/google/cel-go/checker" +) + +// DefaultCostBudget is the default cost budget for a single expression +// evaluation. Aligned with Kubernetes defaults: enough for typical identity +// operations but prevents runaway expressions. +const DefaultCostBudget uint64 = 10_000_000 + +// MaxExpressionLength is the maximum length of a CEL expression string. +const MaxExpressionLength = 10_240 + +// DefaultStringMaxLength is the estimated max length of string values +// (emails, usernames, group names, etc.) used for compile-time cost estimation. +const DefaultStringMaxLength = 256 + +// DefaultListMaxLength is the estimated max length of list values +// (groups, scopes) used for compile-time cost estimation. +const DefaultListMaxLength = 100 + +// CostEstimate holds the estimated cost range for a compiled expression. +type CostEstimate struct { + Min uint64 + Max uint64 +} + +// EstimateCost returns the estimated cost range for a compiled expression. +// This is computed statically at compile time without evaluating the expression. +func (c *Compiler) EstimateCost(result *CompilationResult) (CostEstimate, error) { + costEst, err := c.env.EstimateCost(result.ast, &defaultCostEstimator{}) + if err != nil { + return CostEstimate{}, fmt.Errorf("CEL cost estimation failed: %w", err) + } + + return CostEstimate{Min: costEst.Min, Max: costEst.Max}, nil +} + +// defaultCostEstimator provides size hints for compile-time cost estimation. +// Without these hints, the CEL cost estimator assumes unbounded sizes for +// variables, leading to wildly overestimated max costs. +type defaultCostEstimator struct{} + +func (defaultCostEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstimate { + // Provide size hints for map(string, dyn) variables: identity, request, claims. + // Without these, the estimator assumes lists/strings can be infinitely large. + if element.Path() == nil { + return nil + } + + path := element.Path() + if len(path) == 0 { + return nil + } + + root := path[0] + + switch root { + case "identity", "request", "claims": + // Nested field access (e.g. identity.email, identity.groups) + if len(path) >= 2 { + field := path[1] + switch field { + case "groups", "scopes": + // list(string) fields + return &checker.SizeEstimate{Min: 0, Max: DefaultListMaxLength} + case "email_verified": + // bool field — size is always 1 + return &checker.SizeEstimate{Min: 1, Max: 1} + default: + // string fields (email, username, user_id, client_id, etc.) + return &checker.SizeEstimate{Min: 0, Max: DefaultStringMaxLength} + } + } + // The map itself: number of keys + return &checker.SizeEstimate{Min: 0, Max: 20} + } + + return nil +} + +func (defaultCostEstimator) EstimateCallCost(function, overloadID string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + switch function { + case "dex.emailDomain", "dex.emailLocalPart": + // Simple string split — O(n) where n is string length, bounded. + return &checker.CallEstimate{ + CostEstimate: checker.CostEstimate{Min: 1, Max: 2}, + } + case "dex.groupMatches": + // Iterates over groups list and matches each against a pattern. + return &checker.CallEstimate{ + CostEstimate: checker.CostEstimate{Min: 1, Max: DefaultListMaxLength}, + } + case "dex.groupFilter": + // Builds a set from allowed list, then iterates groups. + return &checker.CallEstimate{ + CostEstimate: checker.CostEstimate{Min: 1, Max: 2 * DefaultListMaxLength}, + } + } + + return nil +} diff --git a/pkg/cel/cost_test.go b/pkg/cel/cost_test.go new file mode 100644 index 0000000000..9a068be406 --- /dev/null +++ b/pkg/cel/cost_test.go @@ -0,0 +1,137 @@ +package cel_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dexcel "github.com/dexidp/dex/pkg/cel" +) + +func TestEstimateCost(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + tests := map[string]struct { + expr string + }{ + "simple bool": { + expr: "true", + }, + "string comparison": { + expr: "identity.email == 'test@example.com'", + }, + "group membership": { + expr: "identity.groups.exists(g, g == 'admin')", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prog, err := compiler.Compile(tc.expr) + require.NoError(t, err) + + est, err := compiler.EstimateCost(prog) + require.NoError(t, err) + assert.True(t, est.Max >= est.Min, "max cost should be >= min cost") + assert.True(t, est.Max <= dexcel.DefaultCostBudget, + "estimated max cost %d should be within default budget %d", est.Max, dexcel.DefaultCostBudget) + }) + } +} + +func TestCompileTimeCostAcceptsSimpleExpressions(t *testing.T) { + vars := append(dexcel.IdentityVariables(), dexcel.RequestVariables()...) + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + tests := map[string]string{ + "literal": "true", + "email endsWith": "identity.email.endsWith('@example.com')", + "group check": "'admin' in identity.groups", + "emailDomain": `dex.emailDomain(identity.email)`, + "groupMatches": `dex.groupMatches(identity.groups, "team:*")`, + "groupFilter": `dex.groupFilter(identity.groups, ["admin", "dev"])`, + "combined policy": `identity.email.endsWith('@example.com') && 'admin' in identity.groups`, + "complex policy": `identity.email.endsWith('@example.com') && + identity.groups.exists(g, g == 'admin') && + request.connector_id == 'okta' && + request.scopes.exists(s, s == 'openid')`, + "filter+map chain": `identity.groups + .filter(g, g.startsWith('team:')) + .map(g, g.replace('team:', '')) + .size() > 0`, + } + + for name, expr := range tests { + t.Run(name, func(t *testing.T) { + _, err := compiler.Compile(expr) + assert.NoError(t, err, "expression should compile within default budget") + }) + } +} + +func TestCompileTimeCostRejection(t *testing.T) { + vars := append(dexcel.IdentityVariables(), dexcel.RequestVariables()...) + + tests := map[string]struct { + budget uint64 + expr string + }{ + "simple exists exceeds tiny budget": { + budget: 1, + expr: "identity.groups.exists(g, g == 'admin')", + }, + "endsWith exceeds tiny budget": { + budget: 2, + expr: "identity.email.endsWith('@example.com')", + }, + "nested comprehension over groups exceeds moderate budget": { + // Two nested iterations over groups: O(n^2) where n=100 → ~280K + budget: 10_000, + expr: `identity.groups.exists(g1, + identity.groups.exists(g2, + g1 != g2 && g1.startsWith(g2) + ) + )`, + }, + "cross-variable comprehension exceeds moderate budget": { + // filter groups then check each against scopes: O(n*m) → ~162K + budget: 10_000, + expr: `identity.groups + .filter(g, g.startsWith('team:')) + .exists(g, request.scopes.exists(s, s == g))`, + }, + "chained filter+map+filter+map exceeds small budget": { + budget: 1000, + expr: `identity.groups + .filter(g, g.startsWith('team:')) + .map(g, g.replace('team:', '')) + .filter(g, g.size() > 3) + .map(g, g.upperAscii()) + .size() > 0`, + }, + "many independent exists exceeds small budget": { + budget: 5000, + expr: `identity.groups.exists(g, g.contains('a')) && + identity.groups.exists(g, g.contains('b')) && + identity.groups.exists(g, g.contains('c')) && + identity.groups.exists(g, g.contains('d')) && + identity.groups.exists(g, g.contains('e'))`, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + compiler, err := dexcel.NewCompiler(vars, dexcel.WithCostBudget(tc.budget)) + require.NoError(t, err) + + _, err = compiler.Compile(tc.expr) + assert.Error(t, err) + assert.Contains(t, err.Error(), "estimated cost") + assert.Contains(t, err.Error(), "exceeds budget") + }) + } +} diff --git a/pkg/cel/doc.go b/pkg/cel/doc.go new file mode 100644 index 0000000000..64c1dbd303 --- /dev/null +++ b/pkg/cel/doc.go @@ -0,0 +1,5 @@ +// Package cel provides a safe, sandboxed CEL (Common Expression Language) +// environment for policy evaluation, claim mapping, and token customization +// in Dex. It includes cost budgets, Kubernetes-grade compatibility guarantees, +// and a curated set of extension libraries. +package cel diff --git a/pkg/cel/library/doc.go b/pkg/cel/library/doc.go new file mode 100644 index 0000000000..1452d2b939 --- /dev/null +++ b/pkg/cel/library/doc.go @@ -0,0 +1,4 @@ +// Package library provides custom CEL function libraries for Dex. +// Each library implements the cel.Library interface and can be registered +// in a CEL environment. +package library diff --git a/pkg/cel/library/email.go b/pkg/cel/library/email.go new file mode 100644 index 0000000000..38fe0dee94 --- /dev/null +++ b/pkg/cel/library/email.go @@ -0,0 +1,73 @@ +package library + +import ( + "strings" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// Email provides email-related CEL functions. +// +// Functions (V1): +// +// dex.emailDomain(email: string) -> string +// Returns the domain portion of an email address. +// Example: dex.emailDomain("user@example.com") == "example.com" +// +// dex.emailLocalPart(email: string) -> string +// Returns the local part of an email address. +// Example: dex.emailLocalPart("user@example.com") == "user" +type Email struct{} + +func (Email) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + cel.Function("dex.emailDomain", + cel.Overload("dex_email_domain_string", + []*cel.Type{cel.StringType}, + cel.StringType, + cel.UnaryBinding(emailDomainImpl), + ), + ), + cel.Function("dex.emailLocalPart", + cel.Overload("dex_email_local_part_string", + []*cel.Type{cel.StringType}, + cel.StringType, + cel.UnaryBinding(emailLocalPartImpl), + ), + ), + } +} + +func (Email) ProgramOptions() []cel.ProgramOption { + return nil +} + +func emailDomainImpl(arg ref.Val) ref.Val { + email, ok := arg.Value().(string) + if !ok { + return types.NewErr("dex.emailDomain: expected string argument") + } + + _, domain, found := strings.Cut(email, "@") + if !found { + return types.String("") + } + + return types.String(domain) +} + +func emailLocalPartImpl(arg ref.Val) ref.Val { + email, ok := arg.Value().(string) + if !ok { + return types.NewErr("dex.emailLocalPart: expected string argument") + } + + localPart, _, found := strings.Cut(email, "@") + if !found { + return types.String(email) + } + + return types.String(localPart) +} diff --git a/pkg/cel/library/email_test.go b/pkg/cel/library/email_test.go new file mode 100644 index 0000000000..d13e73a1dd --- /dev/null +++ b/pkg/cel/library/email_test.go @@ -0,0 +1,106 @@ +package library_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dexcel "github.com/dexidp/dex/pkg/cel" +) + +func TestEmailDomain(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + tests := map[string]struct { + expr string + want string + }{ + "standard email": { + expr: `dex.emailDomain("user@example.com")`, + want: "example.com", + }, + "subdomain": { + expr: `dex.emailDomain("admin@sub.domain.org")`, + want: "sub.domain.org", + }, + "no at sign": { + expr: `dex.emailDomain("nodomain")`, + want: "", + }, + "empty string": { + expr: `dex.emailDomain("")`, + want: "", + }, + "multiple at signs": { + expr: `dex.emailDomain("user@name@example.com")`, + want: "name@example.com", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prog, err := compiler.CompileString(tc.expr) + require.NoError(t, err) + + result, err := dexcel.EvalString(context.Background(), prog, map[string]any{}) + require.NoError(t, err) + assert.Equal(t, tc.want, result) + }) + } +} + +func TestEmailLocalPart(t *testing.T) { + compiler, err := dexcel.NewCompiler(nil) + require.NoError(t, err) + + tests := map[string]struct { + expr string + want string + }{ + "standard email": { + expr: `dex.emailLocalPart("user@example.com")`, + want: "user", + }, + "no at sign": { + expr: `dex.emailLocalPart("justuser")`, + want: "justuser", + }, + "empty string": { + expr: `dex.emailLocalPart("")`, + want: "", + }, + "multiple at signs": { + expr: `dex.emailLocalPart("user@name@example.com")`, + want: "user", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prog, err := compiler.CompileString(tc.expr) + require.NoError(t, err) + + result, err := dexcel.EvalString(context.Background(), prog, map[string]any{}) + require.NoError(t, err) + assert.Equal(t, tc.want, result) + }) + } +} + +func TestEmailDomainWithIdentityVariable(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + prog, err := compiler.CompileString(`dex.emailDomain(identity.email)`) + require.NoError(t, err) + + result, err := dexcel.EvalString(context.Background(), prog, map[string]any{ + "identity": dexcel.IdentityVal{Email: "admin@corp.example.com"}, + }) + require.NoError(t, err) + assert.Equal(t, "corp.example.com", result) +} diff --git a/pkg/cel/library/groups.go b/pkg/cel/library/groups.go new file mode 100644 index 0000000000..fd7f3603f1 --- /dev/null +++ b/pkg/cel/library/groups.go @@ -0,0 +1,123 @@ +package library + +import ( + "path" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" +) + +// Groups provides group-related CEL functions. +// +// Functions (V1): +// +// dex.groupMatches(groups: list(string), pattern: string) -> list(string) +// Returns groups matching a glob pattern. +// Example: dex.groupMatches(["team:dev", "team:ops", "admin"], "team:*") +// +// dex.groupFilter(groups: list(string), allowed: list(string)) -> list(string) +// Returns only groups present in the allowed list. +// Example: dex.groupFilter(["admin", "dev", "ops"], ["admin", "ops"]) +type Groups struct{} + +func (Groups) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + cel.Function("dex.groupMatches", + cel.Overload("dex_group_matches_list_string", + []*cel.Type{cel.ListType(cel.StringType), cel.StringType}, + cel.ListType(cel.StringType), + cel.BinaryBinding(groupMatchesImpl), + ), + ), + cel.Function("dex.groupFilter", + cel.Overload("dex_group_filter_list_list", + []*cel.Type{cel.ListType(cel.StringType), cel.ListType(cel.StringType)}, + cel.ListType(cel.StringType), + cel.BinaryBinding(groupFilterImpl), + ), + ), + } +} + +func (Groups) ProgramOptions() []cel.ProgramOption { + return nil +} + +func groupMatchesImpl(lhs, rhs ref.Val) ref.Val { + groupList, ok := lhs.(traits.Lister) + if !ok { + return types.NewErr("dex.groupMatches: expected list(string) as first argument") + } + + pattern, ok := rhs.Value().(string) + if !ok { + return types.NewErr("dex.groupMatches: expected string pattern as second argument") + } + + iter := groupList.Iterator() + var matched []ref.Val + + for iter.HasNext() == types.True { + item := iter.Next() + + group, ok := item.Value().(string) + if !ok { + continue + } + + ok, err := path.Match(pattern, group) + if err != nil { + return types.NewErr("dex.groupMatches: invalid pattern %q: %v", pattern, err) + } + if ok { + matched = append(matched, types.String(group)) + } + } + + return types.NewRefValList(types.DefaultTypeAdapter, matched) +} + +func groupFilterImpl(lhs, rhs ref.Val) ref.Val { + groupList, ok := lhs.(traits.Lister) + if !ok { + return types.NewErr("dex.groupFilter: expected list(string) as first argument") + } + + allowedList, ok := rhs.(traits.Lister) + if !ok { + return types.NewErr("dex.groupFilter: expected list(string) as second argument") + } + + allowed := make(map[string]struct{}) + iter := allowedList.Iterator() + for iter.HasNext() == types.True { + item := iter.Next() + + s, ok := item.Value().(string) + if !ok { + continue + } + + allowed[s] = struct{}{} + } + + var filtered []ref.Val + iter = groupList.Iterator() + + for iter.HasNext() == types.True { + item := iter.Next() + + group, ok := item.Value().(string) + if !ok { + continue + } + + if _, exists := allowed[group]; exists { + filtered = append(filtered, types.String(group)) + } + } + + return types.NewRefValList(types.DefaultTypeAdapter, filtered) +} diff --git a/pkg/cel/library/groups_test.go b/pkg/cel/library/groups_test.go new file mode 100644 index 0000000000..70a68fb211 --- /dev/null +++ b/pkg/cel/library/groups_test.go @@ -0,0 +1,141 @@ +package library_test + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dexcel "github.com/dexidp/dex/pkg/cel" +) + +func TestGroupMatches(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + tests := map[string]struct { + expr string + groups []string + want []string + }{ + "wildcard pattern": { + expr: `dex.groupMatches(identity.groups, "team:*")`, + groups: []string{"team:dev", "team:ops", "admin"}, + want: []string{"team:dev", "team:ops"}, + }, + "exact match": { + expr: `dex.groupMatches(identity.groups, "admin")`, + groups: []string{"team:dev", "admin", "user"}, + want: []string{"admin"}, + }, + "no matches": { + expr: `dex.groupMatches(identity.groups, "nonexistent")`, + groups: []string{"team:dev", "admin"}, + want: []string{}, + }, + "question mark pattern": { + expr: `dex.groupMatches(identity.groups, "team?")`, + groups: []string{"teamA", "teamB", "teams-long"}, + want: []string{"teamA", "teamB"}, + }, + "match all": { + expr: `dex.groupMatches(identity.groups, "*")`, + groups: []string{"a", "b", "c"}, + want: []string{"a", "b", "c"}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prog, err := compiler.CompileStringList(tc.expr) + require.NoError(t, err) + + out, err := dexcel.Eval(context.Background(), prog, map[string]any{ + "identity": dexcel.IdentityVal{Groups: tc.groups}, + }) + require.NoError(t, err) + + nativeVal, err := out.ConvertToNative(reflect.TypeOf([]string{})) + require.NoError(t, err) + + got, ok := nativeVal.([]string) + require.True(t, ok, "expected []string, got %T", nativeVal) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestGroupMatchesInvalidPattern(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + prog, err := compiler.CompileStringList(`dex.groupMatches(identity.groups, "[invalid")`) + require.NoError(t, err) + + _, err = dexcel.Eval(context.Background(), prog, map[string]any{ + "identity": dexcel.IdentityVal{Groups: []string{"admin"}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid pattern") +} + +func TestGroupFilter(t *testing.T) { + vars := dexcel.IdentityVariables() + compiler, err := dexcel.NewCompiler(vars) + require.NoError(t, err) + + tests := map[string]struct { + expr string + groups []string + want []string + }{ + "filter to allowed": { + expr: `dex.groupFilter(identity.groups, ["admin", "ops"])`, + groups: []string{"admin", "dev", "ops"}, + want: []string{"admin", "ops"}, + }, + "no overlap": { + expr: `dex.groupFilter(identity.groups, ["marketing"])`, + groups: []string{"admin", "dev"}, + want: []string{}, + }, + "all allowed": { + expr: `dex.groupFilter(identity.groups, ["a", "b", "c"])`, + groups: []string{"a", "b", "c"}, + want: []string{"a", "b", "c"}, + }, + "empty allowed list": { + expr: `dex.groupFilter(identity.groups, [])`, + groups: []string{"admin", "dev"}, + want: []string{}, + }, + "preserves order": { + expr: `dex.groupFilter(identity.groups, ["z", "a"])`, + groups: []string{"a", "b", "z"}, + want: []string{"a", "z"}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + prog, err := compiler.CompileStringList(tc.expr) + require.NoError(t, err) + + out, err := dexcel.Eval(context.Background(), prog, map[string]any{ + "identity": dexcel.IdentityVal{Groups: tc.groups}, + }) + require.NoError(t, err) + + nativeVal, err := out.ConvertToNative(reflect.TypeOf([]string{})) + require.NoError(t, err) + + got, ok := nativeVal.([]string) + require.True(t, ok, "expected []string, got %T", nativeVal) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/cel/types.go b/pkg/cel/types.go new file mode 100644 index 0000000000..4e65792290 --- /dev/null +++ b/pkg/cel/types.go @@ -0,0 +1,109 @@ +package cel + +import ( + "github.com/google/cel-go/cel" + + "github.com/dexidp/dex/connector" +) + +// VariableDeclaration declares a named variable and its CEL type +// that will be available in expressions. +type VariableDeclaration struct { + Name string + Type *cel.Type +} + +// IdentityVal is the CEL native type for the identity variable. +// Fields are typed so that the CEL compiler rejects unknown field access +// (e.g. identity.emial) at config load time rather than at evaluation time. +type IdentityVal struct { + UserID string `cel:"user_id"` + Username string `cel:"username"` + PreferredUsername string `cel:"preferred_username"` + Email string `cel:"email"` + EmailVerified bool `cel:"email_verified"` + Groups []string `cel:"groups"` +} + +// RequestVal is the CEL native type for the request variable. +type RequestVal struct { + ClientID string `cel:"client_id"` + ConnectorID string `cel:"connector_id"` + Scopes []string `cel:"scopes"` + RedirectURI string `cel:"redirect_uri"` +} + +// identityTypeName is the CEL type name for IdentityVal. +// Derived by ext.NativeTypes as simplePkgAlias(pkgPath) + "." + structName. +const identityTypeName = "cel.IdentityVal" + +// requestTypeName is the CEL type name for RequestVal. +const requestTypeName = "cel.RequestVal" + +// IdentityVariables provides the 'identity' variable with typed fields. +// +// identity.user_id — string +// identity.username — string +// identity.preferred_username — string +// identity.email — string +// identity.email_verified — bool +// identity.groups — list(string) +func IdentityVariables() []VariableDeclaration { + return []VariableDeclaration{ + {Name: "identity", Type: cel.ObjectType(identityTypeName)}, + } +} + +// RequestVariables provides the 'request' variable with typed fields. +// +// request.client_id — string +// request.connector_id — string +// request.scopes — list(string) +// request.redirect_uri — string +func RequestVariables() []VariableDeclaration { + return []VariableDeclaration{ + {Name: "request", Type: cel.ObjectType(requestTypeName)}, + } +} + +// ClaimsVariable provides a 'claims' map for raw upstream claims. +// Claims remain map(string, dyn) because their shape is genuinely +// unknown — they carry arbitrary upstream IdP data. +// +// claims — map(string, dyn) +func ClaimsVariable() []VariableDeclaration { + return []VariableDeclaration{ + {Name: "claims", Type: cel.MapType(cel.StringType, cel.DynType)}, + } +} + +// IdentityFromConnector converts a connector.Identity to a CEL-compatible IdentityVal. +func IdentityFromConnector(id connector.Identity) IdentityVal { + return IdentityVal{ + UserID: id.UserID, + Username: id.Username, + PreferredUsername: id.PreferredUsername, + Email: id.Email, + EmailVerified: id.EmailVerified, + Groups: id.Groups, + } +} + +// RequestContext represents the authentication/token request context +// available as the 'request' variable in CEL expressions. +type RequestContext struct { + ClientID string + ConnectorID string + Scopes []string + RedirectURI string +} + +// RequestFromContext converts a RequestContext to a CEL-compatible RequestVal. +func RequestFromContext(rc RequestContext) RequestVal { + return RequestVal{ + ClientID: rc.ClientID, + ConnectorID: rc.ConnectorID, + Scopes: rc.Scopes, + RedirectURI: rc.RedirectURI, + } +} diff --git a/pkg/featureflags/doc.go b/pkg/featureflags/doc.go new file mode 100644 index 0000000000..2703329361 --- /dev/null +++ b/pkg/featureflags/doc.go @@ -0,0 +1,3 @@ +// Package featureflags provides a mechanism for toggling experimental or +// optional Dex features via environment variables (DEX_). +package featureflags diff --git a/pkg/featureflags/flag.go b/pkg/featureflags/flag.go new file mode 100644 index 0000000000..98729ac9ed --- /dev/null +++ b/pkg/featureflags/flag.go @@ -0,0 +1,33 @@ +package featureflags + +import ( + "os" + "strconv" + "strings" +) + +type flag struct { + Name string + Default bool +} + +func (f *flag) env() string { + return "DEX_" + strings.ToUpper(f.Name) +} + +func (f *flag) Enabled() bool { + raw := os.Getenv(f.env()) + if raw == "" { + return f.Default + } + + res, err := strconv.ParseBool(raw) + if err != nil { + return f.Default + } + return res +} + +func newFlag(s string, d bool) *flag { + return &flag{Name: s, Default: d} +} diff --git a/pkg/featureflags/set.go b/pkg/featureflags/set.go new file mode 100644 index 0000000000..d394297945 --- /dev/null +++ b/pkg/featureflags/set.go @@ -0,0 +1,27 @@ +package featureflags + +var ( + // EntEnabled enables experimental ent-based engine for the database storages. + // https://entgo.io/ + EntEnabled = newFlag("ent_enabled", false) + + // ExpandEnv can enable or disable env expansion in the config which can be useful in environments where, e.g., + // $ sign is a part of the password for LDAP user. + ExpandEnv = newFlag("expand_env", true) + + // APIConnectorsCRUD allows CRUD operations on connectors through the gRPC API + APIConnectorsCRUD = newFlag("api_connectors_crud", false) + + // ContinueOnConnectorFailure allows the server to start even if some connectors fail to initialize. + ContinueOnConnectorFailure = newFlag("continue_on_connector_failure", true) + + // ConfigDisallowUnknownFields enables to forbid unknown fields in the config while unmarshaling. + ConfigDisallowUnknownFields = newFlag("config_disallow_unknown_fields", false) + + // ClientCredentialGrantEnabledByDefault enables the client_credentials grant type by default + // without requiring explicit configuration in oauth2.grantTypes. + ClientCredentialGrantEnabledByDefault = newFlag("client_credential_grant_enabled_by_default", false) + + // SessionsEnabled enables experimental auth sessions support. + SessionsEnabled = newFlag("sessions_enabled", false) +) diff --git a/pkg/groups/doc.go b/pkg/groups/doc.go new file mode 100644 index 0000000000..f1a21d02b8 --- /dev/null +++ b/pkg/groups/doc.go @@ -0,0 +1,2 @@ +// Package groups contains helper functions related to groups. +package groups diff --git a/pkg/groups/groups.go b/pkg/groups/groups.go index 5dde65ab83..d31a5dee3b 100644 --- a/pkg/groups/groups.go +++ b/pkg/groups/groups.go @@ -1,4 +1,3 @@ -// Package groups contains helper functions related to groups package groups // Filter filters out any groups of given that are not in required. Thus it may diff --git a/pkg/httpclient/doc.go b/pkg/httpclient/doc.go new file mode 100644 index 0000000000..3d028a3a1f --- /dev/null +++ b/pkg/httpclient/doc.go @@ -0,0 +1,3 @@ +// Package httpclient provides a configurable HTTP client constructor with +// support for custom CA certificates, root CAs, and TLS settings. +package httpclient diff --git a/pkg/httpclient/httpclient.go b/pkg/httpclient/httpclient.go new file mode 100644 index 0000000000..671e0e7754 --- /dev/null +++ b/pkg/httpclient/httpclient.go @@ -0,0 +1,64 @@ +package httpclient + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "fmt" + "net" + "net/http" + "os" + "time" +) + +func extractCAs(input []string) [][]byte { + result := make([][]byte, 0, len(input)) + for _, ca := range input { + if ca == "" { + continue + } + + pemData, err := os.ReadFile(ca) + if err != nil { + pemData, err = base64.StdEncoding.DecodeString(ca) + if err != nil { + pemData = []byte(ca) + } + } + + result = append(result, pemData) + } + return result +} + +func NewHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} + for index, rootCABytes := range extractCAs(rootCAs) { + if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { + return nil, fmt.Errorf("rootCAs.%d is not in PEM format, certificate must be "+ + "a PEM encoded string, a base64 encoded bytes that contain PEM encoded string, "+ + "or a path to a PEM encoded certificate", index) + } + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, nil +} diff --git a/pkg/httpclient/httpclient_test.go b/pkg/httpclient/httpclient_test.go new file mode 100644 index 0000000000..6f561c1030 --- /dev/null +++ b/pkg/httpclient/httpclient_test.go @@ -0,0 +1,83 @@ +package httpclient_test + +import ( + "crypto/tls" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/dexidp/dex/pkg/httpclient" +) + +func TestRootCAs(t *testing.T) { + ts, err := NewLocalHTTPSTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello, client") + })) + assert.Nil(t, err) + defer ts.Close() + + runTest := func(name string, certs []string) { + t.Run(name, func(t *testing.T) { + rootCAs := certs + testClient, err := httpclient.NewHTTPClient(rootCAs, false) + assert.Nil(t, err) + + res, err := testClient.Get(ts.URL) + assert.Nil(t, err) + + greeting, err := io.ReadAll(res.Body) + res.Body.Close() + assert.Nil(t, err) + + assert.Equal(t, "Hello, client", string(greeting)) + }) + } + + runTest("From file", []string{"testdata/rootCA.pem"}) + + content, err := os.ReadFile("testdata/rootCA.pem") + assert.NoError(t, err) + runTest("From string", []string{string(content)}) + + contentStr := base64.StdEncoding.EncodeToString(content) + runTest("From bytes", []string{contentStr}) +} + +func TestInsecureSkipVerify(t *testing.T) { + ts, err := NewLocalHTTPSTestServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello, client") + })) + assert.Nil(t, err) + defer ts.Close() + + insecureSkipVerify := true + + testClient, err := httpclient.NewHTTPClient(nil, insecureSkipVerify) + assert.Nil(t, err) + + res, err := testClient.Get(ts.URL) + assert.Nil(t, err) + + greeting, err := io.ReadAll(res.Body) + res.Body.Close() + assert.Nil(t, err) + + assert.Equal(t, "Hello, client", string(greeting)) +} + +func NewLocalHTTPSTestServer(handler http.Handler) (*httptest.Server, error) { + ts := httptest.NewUnstartedServer(handler) + cert, err := tls.LoadX509KeyPair("testdata/server.crt", "testdata/server.key") + if err != nil { + return nil, err + } + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} + ts.StartTLS() + return ts, nil +} diff --git a/pkg/httpclient/readme.md b/pkg/httpclient/readme.md new file mode 100644 index 0000000000..cc26252293 --- /dev/null +++ b/pkg/httpclient/readme.md @@ -0,0 +1,44 @@ +# Regenerate testdata + +### server.csr.cnf + +``` +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn + +[dn] +C=US +ST=RandomState +L=RandomCity +O=RandomOrganization +OU=RandomOrganizationUnit +emailAddress=hello@example.com +CN = localhost +``` + +and + +### v3.ext +``` +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 +``` + +### Then enter the following commands: + +`openssl genrsa -out rootCA.key 2048` + +`openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem -config server.csr.cnf` + +`openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key -config server.csr.cnf` + +`openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 3650 -sha256 -extfile v3.ext` diff --git a/pkg/httpclient/testdata/rootCA.key b/pkg/httpclient/testdata/rootCA.key new file mode 100644 index 0000000000..9c4eeee12a --- /dev/null +++ b/pkg/httpclient/testdata/rootCA.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4dB5aQCjCmMsW71u9F0WNm1TYjXQBZ4p7oNT+BQwCc/MZ2xc +5NexS2O86nbRkw5jwyfAAMSMKRr9s2FluVTHqiln78rg+XUgmrmNT3ZroLmW6QL6 +Ca8dbMPky+tQclZsvMd3HAeCyyrs4pf7wM1AyUJD7H0xAlVD1fsohkg7jhBFUfV+ +q2VMMdnsaV5vFrW/2vPBWz1SNPW/Xm+Ilny7xg9njQLcPMNtVtF+7EPB6sxD6qrj +BC+Kj5zQ3bZOfdrh7yy63dbh/Kh+3NScgO+k+x92HlAjRIvj5y4KrbGZl7CmOth5 +y7fPywApVbDfZRWJChI1PVflOyDdnC+vhMLbHQIDAQABAoIBAEmjrrQrXP/6L3EL +aa+O27uME3Enk1sBpTL+6Ncx3iiU91eS4whNvqeTMvxTGy0VuDrgL6EQd5TAFJP2 +4zF5EFPRhO+R/aPcKnHKqOaM+7RCUZBTRC78SGA70dUeO/HNdVBqy9D8Mg8HRJDw +d0z8om//iB8LBHx6SdDyQtjnnWRKFTzQRurBBoyLe2vPMFtINKtNUkahjc8HE4GO +aIv1LICJUzf4ZnkntKd5cFHZ42R2Tmfj0Y9G9DyJbuSA3+0u5IhYB39Uy6jFxLi8 +I5PoIVhgYZ0aivsVBIviShwQ9kgv6807YBxt22eSNovBDrSp+cAnIF9+p0b3MnkU +aCHSiBECgYEA84lssi6AqfCEsSiQMSM9kMCXJ4KQI/l7pmrIA50+V5HSEby9lg2Y +N6XJ4V4q46t8FcZBjmMvzn9fwiPMRw5e995cVNBQ31a1FX/1Hy6RNtEiLZRnkHI5 +WznY9IxQ+c9JXJeFY1sO0BfO0TS3WvOf1rwqOb92q+cQaItnPQ+4Ya8CgYEA7V7e +IqW3PpO4H+c5hH9egM0BjAxH71C9YpYzZpF9uiPIkuMnJ8nm9bB6RiuDaYCxvrfE +A0h/SQewoYJKL4OfKGjrbG7U4zLMZHIWlf8Za55Zik5BNjvgBqFFrrSgLUGxdRTX +N0+TlWlW1bvJblWpdjIbJbg/6kCU98TzK852fvMCgYAWYa/apElw1MjtGyQ9T9bN +odWCbQ5gMAJ8Jd4h7uaW17DtrmHiE3fEzXjDPItGhzENMz49HsJ7ANvFFNMmSJzT +vNzRcp+sFuTnh+34Iqh32DqC49usu8KnrqZQu0CJ5NICL26z1d+DolyAf47GThOH +gZ2D1yPJ4p9wbDddtj8kwwKBgCFKB68mPG+rOcxHmjppvnAj0A66/i+izBySYf0F +dHNxZ0SqVKhw2VIlgNBsc86M/OB5VyT6utccG/paklrdg6mgJTwcwwBl9GI12dMJ +ZqBAIeCSnvSjKwTjAynALSKLrv5zgMdCArmWf1YUMuilXNG1rzb4AwawLfQdi9jd +6KJfAoGBALFl6ldywl3sGPk9K2xCDYYhb1TNQyheA5YvoZzZ6XCo1q0Lbwy/FamZ +0TSWkoEmGB/Hck3HgtZDRo3CTI1vYfbpAtgI7oD1NA1zMaLulNQxKjH3iVvyb+R7 +ZcIT7EVPZgkUwr0bsp22yVDekh/CHoB6FZPCyoAb8WnfJfooTBzB +-----END RSA PRIVATE KEY----- diff --git a/pkg/httpclient/testdata/rootCA.pem b/pkg/httpclient/testdata/rootCA.pem new file mode 100644 index 0000000000..c03bdac0c0 --- /dev/null +++ b/pkg/httpclient/testdata/rootCA.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID1jCCAr4CCQCG4JBeSi6cDjANBgkqhkiG9w0BAQsFADCBrDELMAkGA1UEBhMC +VVMxFDASBgNVBAgMC1JhbmRvbVN0YXRlMRMwEQYDVQQHDApSYW5kb21DaXR5MRsw +GQYDVQQKDBJSYW5kb21Pcmdhbml6YXRpb24xHzAdBgNVBAsMFlJhbmRvbU9yZ2Fu +aXphdGlvblVuaXQxIDAeBgkqhkiG9w0BCQEWEWhlbGxvQGV4YW1wbGUuY29tMRIw +EAYDVQQDDAlsb2NhbGhvc3QwHhcNMjIxMDA3MjIwNjQwWhcNMzIxMDA0MjIwNjQw +WjCBrDELMAkGA1UEBhMCVVMxFDASBgNVBAgMC1JhbmRvbVN0YXRlMRMwEQYDVQQH +DApSYW5kb21DaXR5MRswGQYDVQQKDBJSYW5kb21Pcmdhbml6YXRpb24xHzAdBgNV +BAsMFlJhbmRvbU9yZ2FuaXphdGlvblVuaXQxIDAeBgkqhkiG9w0BCQEWEWhlbGxv +QGV4YW1wbGUuY29tMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDh0HlpAKMKYyxbvW70XRY2bVNiNdAFninug1P4FDAJ +z8xnbFzk17FLY7zqdtGTDmPDJ8AAxIwpGv2zYWW5VMeqKWfvyuD5dSCauY1Pdmug +uZbpAvoJrx1sw+TL61ByVmy8x3ccB4LLKuzil/vAzUDJQkPsfTECVUPV+yiGSDuO +EEVR9X6rZUwx2expXm8Wtb/a88FbPVI09b9eb4iWfLvGD2eNAtw8w21W0X7sQ8Hq +zEPqquMEL4qPnNDdtk592uHvLLrd1uH8qH7c1JyA76T7H3YeUCNEi+PnLgqtsZmX +sKY62HnLt8/LAClVsN9lFYkKEjU9V+U7IN2cL6+EwtsdAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAN6g0qit/3R2X+KdR0LgRXF/h4qQFgcV6cxnhRAmLIDNJlxKSHqN +IE5+bxzCbkblzGfr/jNPqW0s+yaN4CyMgKNYSzkLBPE4FF+19Uv+dyYfFms3mDJ7 +0rGjS5bCscThWhpaSw20LcwQcr/+X+/fGzJ01dVFK1UOjBKg4d4dMwxklbIkZqIq +siRW0GMy26mgVZ/BSjeh5kEjs6h6H3cJsGl7xYT+BI7wnxHwGeT9tkBgiyT5FwaS +vtdZkBpQ9q8f7FwsEm3woLHdWuOnrtUtVpY/oc6WFGdROQdGzjSk0D3kHs9YhueC +GSzZKrqX+TSIgpPrLYNHX4uxlo5TAwP/5GM= +-----END CERTIFICATE----- diff --git a/pkg/httpclient/testdata/rootCA.srl b/pkg/httpclient/testdata/rootCA.srl new file mode 100644 index 0000000000..214ae68bf1 --- /dev/null +++ b/pkg/httpclient/testdata/rootCA.srl @@ -0,0 +1 @@ +C1B35F0051A641BB diff --git a/pkg/httpclient/testdata/server.crt b/pkg/httpclient/testdata/server.crt new file mode 100644 index 0000000000..9b0f12ec58 --- /dev/null +++ b/pkg/httpclient/testdata/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5TCCA82gAwIBAgIJAMGzXwBRpkG7MA0GCSqGSIb3DQEBCwUAMIGsMQswCQYD +VQQGEwJVUzEUMBIGA1UECAwLUmFuZG9tU3RhdGUxEzARBgNVBAcMClJhbmRvbUNp +dHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXphdGlvbjEfMB0GA1UECwwWUmFuZG9t +T3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3DQEJARYRaGVsbG9AZXhhbXBsZS5j +b20xEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMjEwMDcyMjA3MDhaFw0zMjEwMDQy +MjA3MDhaMIGsMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLUmFuZG9tU3RhdGUxEzAR +BgNVBAcMClJhbmRvbUNpdHkxGzAZBgNVBAoMElJhbmRvbU9yZ2FuaXphdGlvbjEf +MB0GA1UECwwWUmFuZG9tT3JnYW5pemF0aW9uVW5pdDEgMB4GCSqGSIb3DQEJARYR +aGVsbG9AZXhhbXBsZS5jb20xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMuKdpXP87Q7Kg3iafXzvBuVIyV1K5UmMYiN +koztkC5XrCzHaQRS/CoIb7/nUqmtAxx7RL0jzhZ93zBN4HY/Zcnrd9tXoPPxi0mG +ZZWfFU6nN8nOkMHWzEbHVBmhxpfGtwmLcajQ4HrK1TZwJUn6GqclHQRy/gjxkiw5 +KPqzfVOVlA6ht4KdKstKazQkWZ5gdWT4d8yrEy/IT4oaW05xALBMQ7YGjkzWKsSF +6ygXI7xqF9rg9jCnUsPYg4f8ut3N0c00KjsfKOOj2dF/ZyjedQ5c0u4hHmxSo3Ka +0ZTmIrMfbVXgGjxRG2HZXLpPvQKoCf/fOX8Irdr+lahFVKASxN0CAwEAAaOCAQYw +ggECMIHLBgNVHSMEgcMwgcChgbKkga8wgawxCzAJBgNVBAYTAlVTMRQwEgYDVQQI +DAtSYW5kb21TdGF0ZTETMBEGA1UEBwwKUmFuZG9tQ2l0eTEbMBkGA1UECgwSUmFu +ZG9tT3JnYW5pemF0aW9uMR8wHQYDVQQLDBZSYW5kb21Pcmdhbml6YXRpb25Vbml0 +MSAwHgYJKoZIhvcNAQkBFhFoZWxsb0BleGFtcGxlLmNvbTESMBAGA1UEAwwJbG9j +YWxob3N0ggkAhuCQXkounA4wCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwGgYDVR0R +BBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCWmh5ebpkm +v2B1yQgarSCSSkLZ5DZSAJjrPgW2IJqCW2q2D1HworbW1Yn5jqrM9FKGnJfjCyve +zBB5AOlGp+0bsZGgMRMCavgv4QhTThXUoJqqHcfEu4wHndcgrqSadxmV5aisSR4u +gXnjW43o3akby+h1K40RR3vVkpzPaoC3/bgk7WVpfpPiP32E24a01gETozRb/of/ +ATN3JBe0xh+e63CrPX1sago5+u3UETIoOr0fW8M/gU9GApmJiFAXwHag6j54hLCG +23EtVDwmlarG8Pj+i0yru8s22QqzAJi5E0OwR4aB8tqicLKYBVfzyLCOielIBUrK +OkuFKp+VjxQX +-----END CERTIFICATE----- diff --git a/pkg/httpclient/testdata/server.csr b/pkg/httpclient/testdata/server.csr new file mode 100644 index 0000000000..f422a853c3 --- /dev/null +++ b/pkg/httpclient/testdata/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC8jCCAdoCAQAwgawxCzAJBgNVBAYTAlVTMRQwEgYDVQQIDAtSYW5kb21TdGF0 +ZTETMBEGA1UEBwwKUmFuZG9tQ2l0eTEbMBkGA1UECgwSUmFuZG9tT3JnYW5pemF0 +aW9uMR8wHQYDVQQLDBZSYW5kb21Pcmdhbml6YXRpb25Vbml0MSAwHgYJKoZIhvcN +AQkBFhFoZWxsb0BleGFtcGxlLmNvbTESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy4p2lc/ztDsqDeJp9fO8G5UjJXUr +lSYxiI2SjO2QLlesLMdpBFL8Kghvv+dSqa0DHHtEvSPOFn3fME3gdj9lyet321eg +8/GLSYZllZ8VTqc3yc6QwdbMRsdUGaHGl8a3CYtxqNDgesrVNnAlSfoapyUdBHL+ +CPGSLDko+rN9U5WUDqG3gp0qy0prNCRZnmB1ZPh3zKsTL8hPihpbTnEAsExDtgaO +TNYqxIXrKBcjvGoX2uD2MKdSw9iDh/y63c3RzTQqOx8o46PZ0X9nKN51DlzS7iEe +bFKjcprRlOYisx9tVeAaPFEbYdlcuk+9AqgJ/985fwit2v6VqEVUoBLE3QIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBADjuujIFoDJllR6Xo/w7j5vfNOeHO5GSgxF2 +XnuuDOI9Tomi7vURFZNbz3VAYiehpxRxYqLwFoQUwFtux2qRuGyg0P9fP1iQXPUE +QUfFXmvB80uf2bG4lkbUwnmlZLFOEwhGZyPxpvsrxp2Ei2ppkUopCkzOMsSk3m0X +MC50ZsTHOxfkA3r1WmS7oE2c0p0Fvyx+UJw0URAXFvDS1X0ONgww3FxqbBbm9W37 +5N4FZzGAK6j1wzuynKKXrn20YDCANXYH55PZyupfCeSZT0H0AZifWL7rz/G9uqme +RzbIYc/CNQQTympjinBegQdVeB3yjVNZIvpGOuPSKQqhwFtmDFo= +-----END CERTIFICATE REQUEST----- diff --git a/pkg/httpclient/testdata/server.csr.cnf b/pkg/httpclient/testdata/server.csr.cnf new file mode 100644 index 0000000000..6ff57d1a35 --- /dev/null +++ b/pkg/httpclient/testdata/server.csr.cnf @@ -0,0 +1,14 @@ +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn + +[dn] +C=US +ST=RandomState +L=RandomCity +O=RandomOrganization +OU=RandomOrganizationUnit +emailAddress=hello@example.com +CN = localhost diff --git a/pkg/httpclient/testdata/server.key b/pkg/httpclient/testdata/server.key new file mode 100644 index 0000000000..9708e1e6ea --- /dev/null +++ b/pkg/httpclient/testdata/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLinaVz/O0OyoN +4mn187wblSMldSuVJjGIjZKM7ZAuV6wsx2kEUvwqCG+/51KprQMce0S9I84Wfd8w +TeB2P2XJ63fbV6Dz8YtJhmWVnxVOpzfJzpDB1sxGx1QZocaXxrcJi3Go0OB6ytU2 +cCVJ+hqnJR0Ecv4I8ZIsOSj6s31TlZQOobeCnSrLSms0JFmeYHVk+HfMqxMvyE+K +GltOcQCwTEO2Bo5M1irEhesoFyO8ahfa4PYwp1LD2IOH/LrdzdHNNCo7Hyjjo9nR +f2co3nUOXNLuIR5sUqNymtGU5iKzH21V4Bo8URth2Vy6T70CqAn/3zl/CK3a/pWo +RVSgEsTdAgMBAAECggEAU6cxu7q+54kVbKVsdThaTF/MFR4F7oPHAd9lpuQQSOuh +iLngMHXGy6OyAgYZlEDWMYN8KdwoXFgZPaoUIaVGuWk8Vnq6XOgeHfbNk2PRhwT0 +yc1K80/Lnx9XMj2p+EEkgxi7eu12BSGN5ZTLzo6rG50GQwjb3WMjd2d6rybL0GjC +wg2arcBk3sSMYmvZOqlAsaQmtgwkJhvhVkVfEQSD3VKF7g0dh/h3LIPyM0Ff4M67 +KpLMPPwzUJ/0Z4ewAP06mMKUA86R93M+dWs2eh1oBGnRkVQdhCJLXJpuGHZ6BTiB +Ry0AeorHfnVXPbtpUeAq6m5/BBl6qX0ooB08BIFwAQKBgQDqJpTZS/ZzqL6Kcs14 +MyFu+7DungSxQ5oK9ju7EFSosanSk4UEa/lw992kM6nsIMwgSVQgba5zKcVMeSmk +AVbpznegQD1BYCwOGwbGvkJ8jbhPy+WLbbRjWT/E6AItZgUK+fyTIcNvSehcQqsT +fhgWsK7ueZCmLQfVhK1AxtvY3QKBgQDeiKuo8plsH/7IxDn7KVHBOHKPC2ZPzg03 +i7La6zomiRckwwPnhicRSYsjtfCCW6Ms+uzjTEItgFM+5PdrXheeku+z/sExRtZu +emqPqDomixlXDRQ6RN3gnBSk4RU+ROB1u1uBLWXqRz8Gp2zJGRxhHfYt2zefBv4w +/cIuPC3cAQKBgD2UsAkGJWb9tj8LOmama+CYaUwYWvuT3+uKHuNvxBQpxZQQICet +jgjb53rL66Cib4z+PBXbQsoe7jjSlNUBVS5gkq2et31+IZgEG6AhYbMIQrUZ1uD4 +lTybuF289vWhoynj3T2E37VhJq89CWky/HrbNOabKiPKLAlHv5kNs7wxAoGBANEJ +XQbU7J2O6Iy7FyQBSlTQq3wHX1Iz4mJ9DcNrFzK/sEfOEMrZT7WDefpPm984KW3F +P+S766ZGVuxLtMbcmh9RM23HLr8VJbSdtZ/AjO9L1r/Y/1lE+49TzmibLpNRq++r +0WbkuEl8J44ek6fLuMbZmDi3JeZycTCgDlnUGdgBAoGAYdliovtURZCm46t1uE3F +idCLCXCccjkt1hcNGNjck/b0trHA7wOEqICIguoWDlEBTc0PDvHEq6PfKyqptGkj +AgaZTMF/aZiGqlT7VRpBuzxM/uV5xzCg+i2ViaW/p3xq0z2PRljVZiEfe5aWcjiM +ouTtnC3TgmcjhTgGmb48QQE= +-----END PRIVATE KEY----- diff --git a/pkg/httpclient/testdata/v3.ext b/pkg/httpclient/testdata/v3.ext new file mode 100644 index 0000000000..68e35be863 --- /dev/null +++ b/pkg/httpclient/testdata/v3.ext @@ -0,0 +1,8 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 diff --git a/pkg/log/deprecated.go b/pkg/log/deprecated.go deleted file mode 100644 index f20e8b4cb8..0000000000 --- a/pkg/log/deprecated.go +++ /dev/null @@ -1,5 +0,0 @@ -package log - -func Deprecated(logger Logger, f string, args ...interface{}) { - logger.Warnf("Deprecated: "+f, args...) -} diff --git a/pkg/log/logger.go b/pkg/log/logger.go deleted file mode 100644 index 4f3cdd3851..0000000000 --- a/pkg/log/logger.go +++ /dev/null @@ -1,18 +0,0 @@ -// Package log provides a logger interface for logger libraries -// so that dex does not depend on any of them directly. -// It also includes a default implementation using Logrus (used by dex previously). -package log - -// Logger serves as an adapter interface for logger libraries -// so that dex does not depend on any of them directly. -type Logger interface { - Debug(args ...interface{}) - Info(args ...interface{}) - Warn(args ...interface{}) - Error(args ...interface{}) - - Debugf(format string, args ...interface{}) - Infof(format string, args ...interface{}) - Warnf(format string, args ...interface{}) - Errorf(format string, args ...interface{}) -} diff --git a/scripts/git-diff b/scripts/git-diff deleted file mode 100755 index 302ac2ce3e..0000000000 --- a/scripts/git-diff +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -e - -DIFF=$( git diff . ) -if [ "$DIFF" != "" ]; then - echo "$DIFF" >&2 - exit 1 -fi diff --git a/scripts/git-version b/scripts/git-version index 936641cb0b..a78a2716d0 100755 --- a/scripts/git-version +++ b/scripts/git-version @@ -1,15 +1,42 @@ #!/bin/sh -e -# Since this script will be run in a rkt container, use "/bin/sh" instead of "/bin/bash" # parse the current git commit hash -COMMIT=`git rev-parse HEAD` +COMMIT=`git rev-parse --short=8 HEAD` -# check if the current commit has a matching tag -TAG=$(git describe --exact-match --abbrev=0 --tags ${COMMIT} 2> /dev/null || true) +# check if the current commit has a matching tag (filter for v* tags, excluding api/) +TAG=$(git describe --exact-match --abbrev=0 --tags --match="v[0-9]*" 2> /dev/null || true) # use the matching tag as the version, if available if [ -z "$TAG" ]; then - VERSION=$COMMIT + # No exact tag on current commit, find the last version tag and bump minor version + # Get all tags matching v[0-9]*, sort them, and take the last one + LAST_TAG=$(git tag --list "v[0-9]*" --sort=-version:refname | head -1) + + if [ -z "$LAST_TAG" ]; then + # No tags found, use v0.1.0 as fallback + BASE_VERSION="v0.1.0" + else + # Parse the last tag and bump minor version + # Remove 'v' prefix + TAG_WITHOUT_V="${LAST_TAG#v}" + + # Split version into parts (major.minor.patch) + MAJOR=$(echo "$TAG_WITHOUT_V" | cut -d. -f1) + MINOR=$(echo "$TAG_WITHOUT_V" | cut -d. -f2) + PATCH=$(echo "$TAG_WITHOUT_V" | cut -d. -f3) + + # Bump minor version + MINOR=$((MINOR + 1)) + + # Construct base version with bumped minor + BASE_VERSION="v${MAJOR}.${MINOR}.0" + fi + + # Get commit timestamp in YYYYMMDDhhmmss format + TIMESTAMP=$(git log -1 --format=%ci HEAD | sed 's/[-: ]//g' | cut -c1-14) + + # Construct pseudo-version + VERSION="${BASE_VERSION}-${TIMESTAMP}-${COMMIT}" else VERSION=$TAG fi diff --git a/scripts/update-gomplate b/scripts/update-gomplate new file mode 100755 index 0000000000..4f8d59fd3f --- /dev/null +++ b/scripts/update-gomplate @@ -0,0 +1,53 @@ +#!/bin/sh -e +# Script to check for a new gomplate version and update it in Dockerfile + +GOMPLATE_REPO="hairyhenderson/gomplate" +DOCKERFILE="${1:-.}/Dockerfile" + +# Check if Dockerfile exists +if [ ! -f "$DOCKERFILE" ]; then + echo "Error: Dockerfile not found at $DOCKERFILE" + exit 1 +fi + +# Get the latest release version from GitHub +echo "Checking for the latest gomplate version on GitHub..." +LATEST_VERSION=$(curl -s "https://api.github.com/repos/${GOMPLATE_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*"' | head -1 | cut -d'"' -f4) + +if [ -z "$LATEST_VERSION" ]; then + echo "Error: Could not fetch the latest version from GitHub" + exit 1 +fi + +echo "Latest gomplate version: $LATEST_VERSION" + +# Get the current version from Dockerfile +CURRENT_VERSION=$(grep 'ENV GOMPLATE_VERSION' "$DOCKERFILE" | sed 's/.*GOMPLATE_VERSION=//;s/[[:space:]]*$//') + +echo "Current gomplate version in Dockerfile: $CURRENT_VERSION" + +# Check if versions are different +if [ "$LATEST_VERSION" = "$CURRENT_VERSION" ]; then + echo "✓ Already on the latest version ($LATEST_VERSION)" + exit 0 +fi + +echo "✓ New version available: $LATEST_VERSION" +echo "Updating Dockerfile..." + +# Update the Dockerfile - use a more specific pattern to avoid multiple replacements +sed -i '' "s/ENV GOMPLATE_VERSION=.*/ENV GOMPLATE_VERSION=${LATEST_VERSION}/" "$DOCKERFILE" + +if grep -q "ENV GOMPLATE_VERSION=${LATEST_VERSION}" "$DOCKERFILE"; then + echo "✓ Successfully updated Dockerfile to version $LATEST_VERSION" + echo "" + echo "Changes made:" + echo " - GOMPLATE_VERSION: $CURRENT_VERSION → $LATEST_VERSION" +else + echo "Error: Failed to update Dockerfile" + exit 1 +fi + + + + diff --git a/server/api.go b/server/api.go index a68742b3cc..1d2f73ba97 100644 --- a/server/api.go +++ b/server/api.go @@ -2,20 +2,23 @@ package server import ( "context" + "encoding/json" "errors" "fmt" + "log/slog" + "strconv" "golang.org/x/crypto/bcrypt" "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/pkg/featureflags" "github.com/dexidp/dex/server/internal" "github.com/dexidp/dex/storage" ) // apiVersion increases every time a new call is added to the API. Clients should use this info // to determine if the server supports specific features. -const apiVersion = 2 +const apiVersion = 3 const ( // recCost is the recommended bcrypt cost, which balances hash strength and @@ -29,11 +32,12 @@ const ( ) // NewAPI returns a server which implements the gRPC API interface. -func NewAPI(s storage.Storage, logger log.Logger, version string) api.DexServer { +func NewAPI(s storage.Storage, logger *slog.Logger, version string, server *Server) api.DexServer { return dexAPI{ s: s, - logger: logger, + logger: logger.With("component", "api"), version: version, + server: server, } } @@ -41,8 +45,29 @@ type dexAPI struct { api.UnimplementedDexServer s storage.Storage - logger log.Logger + logger *slog.Logger version string + server *Server +} + +func (d dexAPI) GetClient(ctx context.Context, req *api.GetClientReq) (*api.GetClientResp, error) { + c, err := d.s.GetClient(ctx, req.Id) + if err != nil { + return nil, err + } + + return &api.GetClientResp{ + Client: &api.Client{ + Id: c.ID, + Name: c.Name, + Secret: c.Secret, + RedirectUris: c.RedirectURIs, + TrustedPeers: c.TrustedPeers, + Public: c.Public, + LogoUrl: c.LogoURL, + AllowedConnectors: c.AllowedConnectors, + }, + }, nil } func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*api.CreateClientResp, error) { @@ -58,19 +83,20 @@ func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*ap } c := storage.Client{ - ID: req.Client.Id, - Secret: req.Client.Secret, - RedirectURIs: req.Client.RedirectUris, - TrustedPeers: req.Client.TrustedPeers, - Public: req.Client.Public, - Name: req.Client.Name, - LogoURL: req.Client.LogoUrl, - } - if err := d.s.CreateClient(c); err != nil { + ID: req.Client.Id, + Secret: req.Client.Secret, + RedirectURIs: req.Client.RedirectUris, + TrustedPeers: req.Client.TrustedPeers, + Public: req.Client.Public, + Name: req.Client.Name, + LogoURL: req.Client.LogoUrl, + AllowedConnectors: req.Client.AllowedConnectors, + } + if err := d.s.CreateClient(ctx, c); err != nil { if err == storage.ErrAlreadyExists { return &api.CreateClientResp{AlreadyExists: true}, nil } - d.logger.Errorf("api: failed to create client: %v", err) + d.logger.Error("failed to create client", "err", err) return nil, fmt.Errorf("create client: %v", err) } @@ -84,7 +110,7 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap return nil, errors.New("update client: no client ID supplied") } - err := d.s.UpdateClient(req.Id, func(old storage.Client) (storage.Client, error) { + err := d.s.UpdateClient(ctx, req.Id, func(old storage.Client) (storage.Client, error) { if req.RedirectUris != nil { old.RedirectURIs = req.RedirectUris } @@ -97,30 +123,59 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap if req.LogoUrl != "" { old.LogoURL = req.LogoUrl } + if req.AllowedConnectors != nil { + old.AllowedConnectors = req.AllowedConnectors + } return old, nil }) if err != nil { if err == storage.ErrNotFound { return &api.UpdateClientResp{NotFound: true}, nil } - d.logger.Errorf("api: failed to update the client: %v", err) + d.logger.Error("failed to update the client", "err", err) return nil, fmt.Errorf("update client: %v", err) } return &api.UpdateClientResp{}, nil } func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*api.DeleteClientResp, error) { - err := d.s.DeleteClient(req.Id) + err := d.s.DeleteClient(ctx, req.Id) if err != nil { if err == storage.ErrNotFound { return &api.DeleteClientResp{NotFound: true}, nil } - d.logger.Errorf("api: failed to delete client: %v", err) + d.logger.Error("failed to delete client", "err", err) return nil, fmt.Errorf("delete client: %v", err) } return &api.DeleteClientResp{}, nil } +func (d dexAPI) ListClients(ctx context.Context, req *api.ListClientReq) (*api.ListClientResp, error) { + clientList, err := d.s.ListClients(ctx) + if err != nil { + d.logger.Error("failed to list clients", "err", err) + return nil, fmt.Errorf("list clients: %v", err) + } + + clients := make([]*api.ClientInfo, 0, len(clientList)) + for _, client := range clientList { + c := api.ClientInfo{ + Id: client.ID, + Name: client.Name, + RedirectUris: client.RedirectURIs, + TrustedPeers: client.TrustedPeers, + Public: client.Public, + LogoUrl: client.LogoURL, + AllowedConnectors: client.AllowedConnectors, + } + clients = append(clients, &c) + } + + return &api.ListClientResp{ + Clients: clients, + }, nil +} + // checkCost returns an error if the hash provided does not meet lower or upper // bound cost requirements. func checkCost(hash []byte) error { @@ -158,11 +213,11 @@ func (d dexAPI) CreatePassword(ctx context.Context, req *api.CreatePasswordReq) Username: req.Password.Username, UserID: req.Password.UserId, } - if err := d.s.CreatePassword(p); err != nil { + if err := d.s.CreatePassword(ctx, p); err != nil { if err == storage.ErrAlreadyExists { return &api.CreatePasswordResp{AlreadyExists: true}, nil } - d.logger.Errorf("api: failed to create password: %v", err) + d.logger.Error("failed to create password", "err", err) return nil, fmt.Errorf("create password: %v", err) } @@ -195,11 +250,11 @@ func (d dexAPI) UpdatePassword(ctx context.Context, req *api.UpdatePasswordReq) return old, nil } - if err := d.s.UpdatePassword(req.Email, updater); err != nil { + if err := d.s.UpdatePassword(ctx, req.Email, updater); err != nil { if err == storage.ErrNotFound { return &api.UpdatePasswordResp{NotFound: true}, nil } - d.logger.Errorf("api: failed to update password: %v", err) + d.logger.Error("failed to update password", "err", err) return nil, fmt.Errorf("update password: %v", err) } @@ -211,12 +266,12 @@ func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq) return nil, errors.New("no email supplied") } - err := d.s.DeletePassword(req.Email) + err := d.s.DeletePassword(ctx, req.Email) if err != nil { if err == storage.ErrNotFound { return &api.DeletePasswordResp{NotFound: true}, nil } - d.logger.Errorf("api: failed to delete password: %v", err) + d.logger.Error("failed to delete password", "err", err) return nil, fmt.Errorf("delete password: %v", err) } return &api.DeletePasswordResp{}, nil @@ -229,10 +284,24 @@ func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.Versi }, nil } +func (d dexAPI) GetDiscovery(ctx context.Context, req *api.DiscoveryReq) (*api.DiscoveryResp, error) { + discoveryDoc := d.server.constructDiscovery(ctx) + data, err := json.Marshal(discoveryDoc) + if err != nil { + return nil, fmt.Errorf("failed to marshal discovery data: %v", err) + } + resp := api.DiscoveryResp{} + err = json.Unmarshal(data, &resp) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal discovery data: %v", err) + } + return &resp, nil +} + func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) { - passwordList, err := d.s.ListPasswords() + passwordList, err := d.s.ListPasswords(ctx) if err != nil { - d.logger.Errorf("api: failed to list passwords: %v", err) + d.logger.Error("failed to list passwords", "err", err) return nil, fmt.Errorf("list passwords: %v", err) } @@ -260,19 +329,19 @@ func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) return nil, errors.New("no password to verify supplied") } - password, err := d.s.GetPassword(req.Email) + password, err := d.s.GetPassword(ctx, req.Email) if err != nil { if err == storage.ErrNotFound { return &api.VerifyPasswordResp{ NotFound: true, }, nil } - d.logger.Errorf("api: there was an error retrieving the password: %v", err) + d.logger.Error("there was an error retrieving the password", "err", err) return nil, fmt.Errorf("verify password: %v", err) } if err := bcrypt.CompareHashAndPassword(password.Hash, []byte(req.Password)); err != nil { - d.logger.Infof("api: password check failed: %v", err) + d.logger.Info("password check failed", "err", err) return &api.VerifyPasswordResp{ Verified: false, }, nil @@ -285,18 +354,18 @@ func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, error) { id := new(internal.IDTokenSubject) if err := internal.Unmarshal(req.UserId, id); err != nil { - d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err) + d.logger.Error("failed to unmarshal ID Token subject", "err", err) return nil, err } - offlineSessions, err := d.s.GetOfflineSessions(id.UserId, id.ConnId) + offlineSessions, err := d.s.GetOfflineSessions(ctx, id.UserId, id.ConnId) if err != nil { if err == storage.ErrNotFound { // This means that this user-client pair does not have a refresh token yet. // An empty list should be returned instead of an error. return &api.ListRefreshResp{}, nil } - d.logger.Errorf("api: failed to list refresh tokens %t here : %v", err == storage.ErrNotFound, err) + d.logger.Error("failed to list refresh tokens here", "err", err) return nil, err } @@ -319,7 +388,7 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api. func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) { id := new(internal.IDTokenSubject) if err := internal.Unmarshal(req.UserId, id); err != nil { - d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err) + d.logger.Error("failed to unmarshal ID Token subject", "err", err) return nil, err } @@ -330,7 +399,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (* updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) { refreshRef := old.Refresh[req.ClientId] if refreshRef == nil || refreshRef.ID == "" { - d.logger.Errorf("api: refresh token issued to client %q for user %q not found for deletion", req.ClientId, id.UserId) + d.logger.Error("refresh token issued to client not found for deletion", "client_id", req.ClientId, "user_id", id.UserId) notFound = true return old, storage.ErrNotFound } @@ -343,11 +412,11 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (* return old, nil } - if err := d.s.UpdateOfflineSessions(id.UserId, id.ConnId, updater); err != nil { + if err := d.s.UpdateOfflineSessions(ctx, id.UserId, id.ConnId, updater); err != nil { if err == storage.ErrNotFound { return &api.RevokeRefreshResp{NotFound: true}, nil } - d.logger.Errorf("api: failed to update offline session object: %v", err) + d.logger.Error("failed to update offline session object", "err", err) return nil, err } @@ -359,10 +428,186 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (* // // TODO(ericchiang): we don't have any good recourse if this call fails. // Consider garbage collection of refresh tokens with no associated ref. - if err := d.s.DeleteRefresh(refreshID); err != nil { - d.logger.Errorf("failed to delete refresh token: %v", err) + if err := d.s.DeleteRefresh(ctx, refreshID); err != nil { + d.logger.Error("failed to delete refresh token", "err", err) return nil, err } return &api.RevokeRefreshResp{}, nil } + +func (d dexAPI) CreateConnector(ctx context.Context, req *api.CreateConnectorReq) (*api.CreateConnectorResp, error) { + if !featureflags.APIConnectorsCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APIConnectorsCRUD.Name) + } + + if req.Connector.Id == "" { + return nil, errors.New("no id supplied") + } + + if req.Connector.Type == "" { + return nil, errors.New("no type supplied") + } + + if req.Connector.Name == "" { + return nil, errors.New("no name supplied") + } + + if len(req.Connector.Config) == 0 { + return nil, errors.New("no config supplied") + } + + if !json.Valid(req.Connector.Config) { + return nil, errors.New("invalid config supplied") + } + + for _, gt := range req.Connector.GrantTypes { + if !ConnectorGrantTypes[gt] { + return nil, fmt.Errorf("unknown grant type %q", gt) + } + } + + c := storage.Connector{ + ID: req.Connector.Id, + Name: req.Connector.Name, + Type: req.Connector.Type, + ResourceVersion: "1", + Config: req.Connector.Config, + GrantTypes: req.Connector.GrantTypes, + } + if err := d.s.CreateConnector(ctx, c); err != nil { + if err == storage.ErrAlreadyExists { + return &api.CreateConnectorResp{AlreadyExists: true}, nil + } + d.logger.Error("api: failed to create connector", "err", err) + return nil, fmt.Errorf("create connector: %v", err) + } + + // Make sure we don't reuse stale entries in the cache + if d.server != nil { + d.server.CloseConnector(req.Connector.Id) + } + + return &api.CreateConnectorResp{}, nil +} + +func (d dexAPI) UpdateConnector(ctx context.Context, req *api.UpdateConnectorReq) (*api.UpdateConnectorResp, error) { + if !featureflags.APIConnectorsCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APIConnectorsCRUD.Name) + } + + if req.Id == "" { + return nil, errors.New("no email supplied") + } + + hasUpdate := len(req.NewConfig) != 0 || + req.NewName != "" || + req.NewType != "" || + req.NewGrantTypes != nil + if !hasUpdate { + return nil, errors.New("nothing to update") + } + + if len(req.NewConfig) != 0 && !json.Valid(req.NewConfig) { + return nil, errors.New("invalid config supplied") + } + + if req.NewGrantTypes != nil { + for _, gt := range req.NewGrantTypes.GrantTypes { + if !ConnectorGrantTypes[gt] { + return nil, fmt.Errorf("unknown grant type %q", gt) + } + } + } + + updater := func(old storage.Connector) (storage.Connector, error) { + if req.NewType != "" { + old.Type = req.NewType + } + + if req.NewName != "" { + old.Name = req.NewName + } + + if len(req.NewConfig) != 0 { + old.Config = req.NewConfig + } + + if req.NewGrantTypes != nil { + old.GrantTypes = req.NewGrantTypes.GrantTypes + } + + if rev, err := strconv.Atoi(defaultTo(old.ResourceVersion, "0")); err == nil { + old.ResourceVersion = strconv.Itoa(rev + 1) + } + + return old, nil + } + + if err := d.s.UpdateConnector(ctx, req.Id, updater); err != nil { + if err == storage.ErrNotFound { + return &api.UpdateConnectorResp{NotFound: true}, nil + } + d.logger.Error("api: failed to update connector", "err", err) + return nil, fmt.Errorf("update connector: %v", err) + } + + return &api.UpdateConnectorResp{}, nil +} + +func (d dexAPI) DeleteConnector(ctx context.Context, req *api.DeleteConnectorReq) (*api.DeleteConnectorResp, error) { + if !featureflags.APIConnectorsCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APIConnectorsCRUD.Name) + } + + if req.Id == "" { + return nil, errors.New("no id supplied") + } + + err := d.s.DeleteConnector(ctx, req.Id) + if err != nil { + if err == storage.ErrNotFound { + return &api.DeleteConnectorResp{NotFound: true}, nil + } + d.logger.Error("api: failed to delete connector", "err", err) + return nil, fmt.Errorf("delete connector: %v", err) + } + + return &api.DeleteConnectorResp{}, nil +} + +func (d dexAPI) ListConnectors(ctx context.Context, req *api.ListConnectorReq) (*api.ListConnectorResp, error) { + if !featureflags.APIConnectorsCRUD.Enabled() { + return nil, fmt.Errorf("%s feature flag is not enabled", featureflags.APIConnectorsCRUD.Name) + } + + connectorList, err := d.s.ListConnectors(ctx) + if err != nil { + d.logger.Error("api: failed to list connectors", "err", err) + return nil, fmt.Errorf("list connectors: %v", err) + } + + connectors := make([]*api.Connector, 0, len(connectorList)) + for _, connector := range connectorList { + c := api.Connector{ + Id: connector.ID, + Name: connector.Name, + Type: connector.Type, + Config: connector.Config, + GrantTypes: connector.GrantTypes, + } + connectors = append(connectors, &c) + } + + return &api.ListConnectorResp{ + Connectors: connectors, + }, nil +} + +func defaultTo[T comparable](v, def T) T { + var zeroT T + if v == zeroT { + return def + } + return v +} diff --git a/server/api_cache_test.go b/server/api_cache_test.go new file mode 100644 index 0000000000..64564c469f --- /dev/null +++ b/server/api_cache_test.go @@ -0,0 +1,133 @@ +package server + +import ( + "context" + "encoding/json" + "testing" + + "github.com/dexidp/dex/api/v2" + "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/connector/mock" + "github.com/dexidp/dex/storage/memory" +) + +func TestConnectorCacheInvalidation(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + serv := &Server{ + storage: s, + logger: logger, + connectors: make(map[string]Connector), + } + + apiServer := NewAPI(s, logger, "test", serv) + ctx := context.Background() + + connID := "mock-conn" + + // 1. Create a connector via API + config1 := mock.PasswordConfig{ + Username: "user", + Password: "first-password", + } + config1Bytes, _ := json.Marshal(config1) + + _, err := apiServer.CreateConnector(ctx, &api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connID, + Type: "mockPassword", + Name: "Mock", + Config: config1Bytes, + }, + }) + if err != nil { + t.Fatalf("failed to create connector: %v", err) + } + + // 2. Load it into server cache + c1, err := serv.getConnector(ctx, connID) + if err != nil { + t.Fatalf("failed to get connector: %v", err) + } + + pc1 := c1.Connector.(connector.PasswordConnector) + _, valid, err := pc1.Login(ctx, connector.Scopes{}, "user", "first-password") + if err != nil || !valid { + t.Fatalf("failed to login with first password: %v", err) + } + + // 3. Delete it via API + _, err = apiServer.DeleteConnector(ctx, &api.DeleteConnectorReq{Id: connID}) + if err != nil { + t.Fatalf("failed to delete connector: %v", err) + } + + // 4. Create it again with different password + config2 := mock.PasswordConfig{ + Username: "user", + Password: "second-password", + } + config2Bytes, _ := json.Marshal(config2) + + _, err = apiServer.CreateConnector(ctx, &api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connID, + Type: "mockPassword", + Name: "Mock", + Config: config2Bytes, + }, + }) + if err != nil { + t.Fatalf("failed to create connector: %v", err) + } + + // 5. Load it again + c2, err := serv.getConnector(ctx, connID) + if err != nil { + t.Fatalf("failed to get connector second time: %v", err) + } + + pc2 := c2.Connector.(connector.PasswordConnector) + + // If the fix works, it should now use the second password. + _, valid2, err := pc2.Login(ctx, connector.Scopes{}, "user", "second-password") + if err != nil || !valid2 { + t.Errorf("failed to login with second password, cache might still be stale") + } + + _, valid1, _ := pc2.Login(ctx, connector.Scopes{}, "user", "first-password") + if valid1 { + t.Errorf("unexpectedly logged in with first password, cache is definitely stale") + } + + // 6. Update it via API with a third password + config3 := mock.PasswordConfig{ + Username: "user", + Password: "third-password", + } + config3Bytes, _ := json.Marshal(config3) + + _, err = apiServer.UpdateConnector(ctx, &api.UpdateConnectorReq{ + Id: connID, + NewConfig: config3Bytes, + }) + if err != nil { + t.Fatalf("failed to update connector: %v", err) + } + + // 7. Load it again + c3, err := serv.getConnector(ctx, connID) + if err != nil { + t.Fatalf("failed to get connector third time: %v", err) + } + + pc3 := c3.Connector.(connector.PasswordConnector) + + _, valid3, err := pc3.Login(ctx, connector.Scopes{}, "user", "third-password") + if err != nil || !valid3 { + t.Errorf("failed to login with third password, UpdateConnector might be missing cache invalidation") + } +} diff --git a/server/api_test.go b/server/api_test.go index 01c59cf875..09cfa6783f 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -1,18 +1,17 @@ package server import ( - "context" + "log/slog" "net" - "os" + "slices" + "strings" "testing" "time" - "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/pkg/log" "github.com/dexidp/dex/server/internal" "github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage/memory" @@ -29,20 +28,24 @@ type apiClient struct { Close func() } +func newLogger(t *testing.T) *slog.Logger { + return slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelDebug})) +} + // newAPI constructs a gRCP client connected to a backing server. -func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient { +func newAPI(t *testing.T, s storage.Storage, logger *slog.Logger) *apiClient { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } serv := grpc.NewServer() - api.RegisterDexServer(serv, NewAPI(s, logger, "test")) + api.RegisterDexServer(serv, NewAPI(s, logger, "test", nil)) go serv.Serve(l) - // Dial will retry automatically if the serv.Serve() goroutine + // NewClient will retry automatically if the serv.Serve() goroutine // hasn't started yet. - conn, err := grpc.Dial(l.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(l.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatal(err) } @@ -59,17 +62,14 @@ func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient { // Attempts to create, update and delete a test Password func TestPassword(t *testing.T) { - logger := &logrus.Logger{ - Out: os.Stderr, - Formatter: &logrus.TextFormatter{DisableColors: true}, - Level: logrus.DebugLevel, - } - + logger := newLogger(t) s := memory.New(logger) - client := newAPI(s, logger, t) + + client := newAPI(t, s, logger) defer client.Close() - ctx := context.Background() + ctx := t.Context() + email := "test@example.com" p := api.Password{ Email: email, @@ -152,7 +152,7 @@ func TestPassword(t *testing.T) { t.Fatalf("Unable to update password: %v", err) } - pass, err := s.GetPassword(updateReq.Email) + pass, err := s.GetPassword(ctx, updateReq.Email) if err != nil { t.Fatalf("Unable to retrieve password: %v", err) } @@ -172,14 +172,10 @@ func TestPassword(t *testing.T) { // Ensures checkCost returns expected values func TestCheckCost(t *testing.T) { - logger := &logrus.Logger{ - Out: os.Stderr, - Formatter: &logrus.TextFormatter{DisableColors: true}, - Level: logrus.DebugLevel, - } - + logger := newLogger(t) s := memory.New(logger) - client := newAPI(s, logger, t) + + client := newAPI(t, s, logger) defer client.Close() tests := []struct { @@ -229,17 +225,13 @@ func TestCheckCost(t *testing.T) { // Attempts to list and revoke an existing refresh token. func TestRefreshToken(t *testing.T) { - logger := &logrus.Logger{ - Out: os.Stderr, - Formatter: &logrus.TextFormatter{DisableColors: true}, - Level: logrus.DebugLevel, - } - + logger := newLogger(t) s := memory.New(logger) - client := newAPI(s, logger, t) + + client := newAPI(t, s, logger) defer client.Close() - ctx := context.Background() + ctx := t.Context() // Creating a storage with an existing refresh token and offline session for the user. id := storage.NewID() @@ -262,7 +254,7 @@ func TestRefreshToken(t *testing.T) { ConnectorData: []byte(`{"some":"data"}`), } - if err := s.CreateRefresh(r); err != nil { + if err := s.CreateRefresh(ctx, r); err != nil { t.Fatalf("create refresh token: %v", err) } @@ -280,7 +272,7 @@ func TestRefreshToken(t *testing.T) { } session.Refresh[tokenRef.ClientID] = &tokenRef - if err := s.CreateOfflineSessions(session); err != nil { + if err := s.CreateOfflineSessions(ctx, session); err != nil { t.Fatalf("create offline session: %v", err) } @@ -337,21 +329,18 @@ func TestRefreshToken(t *testing.T) { } if resp, _ := client.ListRefresh(ctx, &listReq); len(resp.RefreshTokens) != 0 { - t.Fatalf("Refresh token returned inspite of revoking it.") + t.Fatalf("Refresh token returned in spite of revoking it.") } } func TestUpdateClient(t *testing.T) { - logger := &logrus.Logger{ - Out: os.Stderr, - Formatter: &logrus.TextFormatter{DisableColors: true}, - Level: logrus.DebugLevel, - } - + logger := newLogger(t) s := memory.New(logger) - client := newAPI(s, logger, t) + + client := newAPI(t, s, logger) defer client.Close() - ctx := context.Background() + + ctx := t.Context() createClient := func(t *testing.T, clientId string) { resp, err := client.CreateClient(ctx, &api.CreateClientReq{ @@ -464,7 +453,7 @@ func TestUpdateClient(t *testing.T) { t.Errorf("expected in response NotFound: %t", tc.want.NotFound) } - client, err := s.GetClient(tc.req.Id) + client, err := s.GetClient(ctx, tc.req.Id) if err != nil { t.Errorf("no client found in the storage: %v", err) } @@ -479,13 +468,13 @@ func TestUpdateClient(t *testing.T) { t.Errorf("expected stored client with LogoURL: %s, found %s", tc.req.LogoUrl, client.LogoURL) } for _, redirectURI := range tc.req.RedirectUris { - found := find(redirectURI, client.RedirectURIs) + found := slices.Contains(client.RedirectURIs, redirectURI) if !found { t.Errorf("expected redirect URI: %s", redirectURI) } } for _, peer := range tc.req.TrustedPeers { - found := find(peer, client.TrustedPeers) + found := slices.Contains(client.TrustedPeers, peer) if !found { t.Errorf("expected trusted peer: %s", peer) } @@ -499,11 +488,439 @@ func TestUpdateClient(t *testing.T) { } } -func find(item string, items []string) bool { - for _, i := range items { - if item == i { - return true +func TestCreateConnector(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + connectorID := "connector123" + connectorName := "TestConnector" + connectorType := "TestType" + connectorConfig := []byte(`{"key": "value"}`) + + createReq := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connectorID, + Name: connectorName, + Type: connectorType, + Config: connectorConfig, + }, + } + + // Test valid connector creation + if resp, err := client.CreateConnector(ctx, &createReq); err != nil || resp.AlreadyExists { + if err != nil { + t.Fatalf("Unable to create connector: %v", err) + } else if resp.AlreadyExists { + t.Fatalf("Unable to create connector since %s already exists", connectorID) + } + t.Fatalf("Unable to create connector: %v", err) + } + + // Test creating the same connector again (expecting failure) + if resp, _ := client.CreateConnector(ctx, &createReq); !resp.AlreadyExists { + t.Fatalf("Created connector %s twice", connectorID) + } + + createReq.Connector.Config = []byte("invalid_json") + + // Test invalid JSON config + if _, err := client.CreateConnector(ctx, &createReq); err == nil { + t.Fatal("Expected an error for invalid JSON config, but none occurred") + } else if !strings.Contains(err.Error(), "invalid config supplied") { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestUpdateConnector(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + connectorID := "connector123" + newConnectorName := "UpdatedConnector" + newConnectorType := "UpdatedType" + newConnectorConfig := []byte(`{"updated_key": "updated_value"}`) + + // Create a connector for testing + createReq := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connectorID, + Name: "TestConnector", + Type: "TestType", + Config: []byte(`{"key": "value"}`), + }, + } + client.CreateConnector(ctx, &createReq) + + updateReq := api.UpdateConnectorReq{ + Id: connectorID, + NewName: newConnectorName, + NewType: newConnectorType, + NewConfig: newConnectorConfig, + } + + // Test valid connector update + if _, err := client.UpdateConnector(ctx, &updateReq); err != nil { + t.Fatalf("Unable to update connector: %v", err) + } + + resp, err := client.ListConnectors(ctx, &api.ListConnectorReq{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + for _, connector := range resp.Connectors { + if connector.Id == connectorID { + if connector.Name != newConnectorName { + t.Fatal("connector name should have been updated") + } + if string(connector.Config) != string(newConnectorConfig) { + t.Fatal("connector config should have been updated") + } + if connector.Type != newConnectorType { + t.Fatal("connector type should have been updated") + } + } + } + + updateReq.NewConfig = []byte("invalid_json") + + // Test invalid JSON config in update request + if _, err := client.UpdateConnector(ctx, &updateReq); err == nil { + t.Fatal("Expected an error for invalid JSON config in update, but none occurred") + } else if !strings.Contains(err.Error(), "invalid config supplied") { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestUpdateConnectorGrantTypes(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + connectorID := "connector-gt" + + // Create a connector without grant types + createReq := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connectorID, + Name: "TestConnector", + Type: "TestType", + Config: []byte(`{"key": "value"}`), + }, + } + _, err := client.CreateConnector(ctx, &createReq) + if err != nil { + t.Fatalf("failed to create connector: %v", err) + } + + // Set grant types + _, err = client.UpdateConnector(ctx, &api.UpdateConnectorReq{ + Id: connectorID, + NewGrantTypes: &api.GrantTypes{GrantTypes: []string{"authorization_code", "refresh_token"}}, + }) + if err != nil { + t.Fatalf("failed to update connector grant types: %v", err) + } + + resp, err := client.ListConnectors(ctx, &api.ListConnectorReq{}) + if err != nil { + t.Fatalf("failed to list connectors: %v", err) + } + for _, c := range resp.Connectors { + if c.Id == connectorID { + if !slices.Equal(c.GrantTypes, []string{"authorization_code", "refresh_token"}) { + t.Fatalf("expected grant types [authorization_code refresh_token], got %v", c.GrantTypes) + } + } + } + + // Clear grant types by passing empty GrantTypes message + _, err = client.UpdateConnector(ctx, &api.UpdateConnectorReq{ + Id: connectorID, + NewGrantTypes: &api.GrantTypes{}, + }) + if err != nil { + t.Fatalf("failed to clear connector grant types: %v", err) + } + + resp, err = client.ListConnectors(ctx, &api.ListConnectorReq{}) + if err != nil { + t.Fatalf("failed to list connectors: %v", err) + } + for _, c := range resp.Connectors { + if c.Id == connectorID { + if len(c.GrantTypes) != 0 { + t.Fatalf("expected empty grant types after clear, got %v", c.GrantTypes) + } + } + } + + // Reject invalid grant type on update + _, err = client.UpdateConnector(ctx, &api.UpdateConnectorReq{ + Id: connectorID, + NewGrantTypes: &api.GrantTypes{GrantTypes: []string{"bogus"}}, + }) + if err == nil { + t.Fatal("expected error for invalid grant type, got nil") + } + if !strings.Contains(err.Error(), `unknown grant type "bogus"`) { + t.Fatalf("unexpected error: %v", err) + } + + // Reject invalid grant type on create + _, err = client.CreateConnector(ctx, &api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: "bad-gt", + Name: "Bad", + Type: "TestType", + Config: []byte(`{}`), + GrantTypes: []string{"invalid_type"}, + }, + }) + if err == nil { + t.Fatal("expected error for invalid grant type on create, got nil") + } + if !strings.Contains(err.Error(), `unknown grant type "invalid_type"`) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDeleteConnector(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + connectorID := "connector123" + + // Create a connector for testing + createReq := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: connectorID, + Name: "TestConnector", + Type: "TestType", + Config: []byte(`{"key": "value"}`), + }, + } + client.CreateConnector(ctx, &createReq) + + deleteReq := api.DeleteConnectorReq{ + Id: connectorID, + } + + // Test valid connector deletion + if _, err := client.DeleteConnector(ctx, &deleteReq); err != nil { + t.Fatalf("Unable to delete connector: %v", err) + } + + // Test non existent connector deletion + resp, err := client.DeleteConnector(ctx, &deleteReq) + if err != nil { + t.Fatalf("Unable to delete connector: %v", err) + } + + if !resp.NotFound { + t.Fatal("Should return not found") + } +} + +func TestListConnectors(t *testing.T) { + t.Setenv("DEX_API_CONNECTORS_CRUD", "true") + + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + // Create connectors for testing + createReq1 := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: "connector1", + Name: "Connector1", + Type: "Type1", + Config: []byte(`{"key": "value1"}`), + }, + } + client.CreateConnector(ctx, &createReq1) + + createReq2 := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: "connector2", + Name: "Connector2", + Type: "Type2", + Config: []byte(`{"key": "value2"}`), + }, + } + client.CreateConnector(ctx, &createReq2) + + listReq := api.ListConnectorReq{} + + // Test listing connectors + if resp, err := client.ListConnectors(ctx, &listReq); err != nil { + t.Fatalf("Unable to list connectors: %v", err) + } else if len(resp.Connectors) != 2 { // Check the number of connectors in the response + t.Fatalf("Expected 2 connectors, found %d", len(resp.Connectors)) + } +} + +func TestMissingConnectorsCRUDFeatureFlag(t *testing.T) { + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + // Create connectors for testing + createReq1 := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: "connector1", + Name: "Connector1", + Type: "Type1", + Config: []byte(`{"key": "value1"}`), + }, + } + client.CreateConnector(ctx, &createReq1) + + createReq2 := api.CreateConnectorReq{ + Connector: &api.Connector{ + Id: "connector2", + Name: "Connector2", + Type: "Type2", + Config: []byte(`{"key": "value2"}`), + }, + } + client.CreateConnector(ctx, &createReq2) + + listReq := api.ListConnectorReq{} + + if _, err := client.ListConnectors(ctx, &listReq); err == nil { + t.Fatal("ListConnectors should have returned an error") + } +} + +func TestListClients(t *testing.T) { + logger := newLogger(t) + s := memory.New(logger) + + client := newAPI(t, s, logger) + defer client.Close() + + ctx := t.Context() + + // List Clients + listResp, err := client.ListClients(ctx, &api.ListClientReq{}) + if err != nil { + t.Fatalf("Unable to list clients: %v", err) + } + if len(listResp.Clients) != 0 { + t.Fatalf("Expected 0 clients, got %d", len(listResp.Clients)) + } + + client1 := &api.Client{ + Id: "client1", + Secret: "secret1", + RedirectUris: []string{"http://localhost:8080/callback"}, + TrustedPeers: []string{"peer1"}, + Public: false, + Name: "Test Client 1", + LogoUrl: "http://example.com/logo1.png", + } + + client2 := &api.Client{ + Id: "client2", + Secret: "secret2", + RedirectUris: []string{"http://localhost:8081/callback"}, + TrustedPeers: []string{"peer2"}, + Public: true, + Name: "Test Client 2", + LogoUrl: "http://example.com/logo2.png", + } + + _, err = client.CreateClient(ctx, &api.CreateClientReq{Client: client1}) + if err != nil { + t.Fatalf("Unable to create client1: %v", err) + } + + _, err = client.CreateClient(ctx, &api.CreateClientReq{Client: client2}) + if err != nil { + t.Fatalf("Unable to create client2: %v", err) + } + + listResp, err = client.ListClients(ctx, &api.ListClientReq{}) + if err != nil { + t.Fatalf("Unable to list clients: %v", err) + } + + if len(listResp.Clients) != 2 { + t.Fatalf("Expected 2 clients, got %d", len(listResp.Clients)) + } + + clientMap := make(map[string]*api.ClientInfo) + for _, c := range listResp.Clients { + clientMap[c.Id] = c + } + + if c1, exists := clientMap["client1"]; !exists { + t.Fatal("client1 not found in list") + } else { + if c1.Name != "Test Client 1" { + t.Errorf("Expected client1 name 'Test Client 1', got '%s'", c1.Name) + } + if len(c1.RedirectUris) != 1 || c1.RedirectUris[0] != "http://localhost:8080/callback" { + t.Errorf("Expected client1 redirect URIs ['http://localhost:8080/callback'], got %v", c1.RedirectUris) + } + if c1.Public != false { + t.Errorf("Expected client1 public false, got %v", c1.Public) + } + if c1.LogoUrl != "http://example.com/logo1.png" { + t.Errorf("Expected client1 logo URL 'http://example.com/logo1.png', got '%s'", c1.LogoUrl) + } + } + + if c2, exists := clientMap["client2"]; !exists { + t.Fatal("client2 not found in list") + } else { + if c2.Name != "Test Client 2" { + t.Errorf("Expected client2 name 'Test Client 2', got '%s'", c2.Name) + } + if len(c2.RedirectUris) != 1 || c2.RedirectUris[0] != "http://localhost:8081/callback" { + t.Errorf("Expected client2 redirect URIs ['http://localhost:8081/callback'], got %v", c2.RedirectUris) + } + if c2.Public != true { + t.Errorf("Expected client2 public true, got %v", c2.Public) + } + if c2.LogoUrl != "http://example.com/logo2.png" { + t.Errorf("Expected client2 logo URL 'http://example.com/logo2.png', got '%s'", c2.LogoUrl) } } - return false } diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go index 95fed3b3c3..b9fb652aae 100644 --- a/server/deviceflowhandlers.go +++ b/server/deviceflowhandlers.go @@ -11,9 +11,6 @@ import ( "strings" "time" - "golang.org/x/net/html" - - "github.com/dexidp/dex/pkg/log" "github.com/dexidp/dex/storage" ) @@ -49,7 +46,7 @@ func (s *Server) handleDeviceExchange(w http.ResponseWriter, r *http.Request) { invalidAttempt = false } if err := s.templates.device(r, w, s.getDeviceVerificationURI(), userCode, invalidAttempt); err != nil { - s.logger.Errorf("Server template error: %v", err) + s.logger.ErrorContext(r.Context(), "server template error", "err", err) s.renderError(r, w, http.StatusNotFound, "Page not found") } default: @@ -58,13 +55,14 @@ func (s *Server) handleDeviceExchange(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() pollIntervalSeconds := 5 switch r.Method { case http.MethodPost: err := r.ParseForm() if err != nil { - s.logger.Errorf("Could not parse Device Request body: %v", err) + s.logger.ErrorContext(r.Context(), "could not parse Device Request body", "err", err) s.tokenErrHelper(w, errInvalidRequest, "", http.StatusNotFound) return } @@ -85,7 +83,13 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) { return } - s.logger.Infof("Received device request for client %v with scopes %v", clientID, scopes) + if len(scopes) == 0 { + // per RFC8628 section 3.1, https://datatracker.ietf.org/doc/html/rfc8628#section-3.1 + // scope is optional but dex requires that it is always at least 'openid' so default it + scopes = []string{"openid"} + } + + s.logger.InfoContext(r.Context(), "received device request", "client_id", clientID, "scoped", scopes) // Make device code deviceCode := storage.NewDeviceCode() @@ -106,8 +110,8 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) { Expiry: expireTime, } - if err := s.storage.CreateDeviceRequest(deviceReq); err != nil { - s.logger.Errorf("Failed to store device request; %v", err) + if err := s.storage.CreateDeviceRequest(ctx, deviceReq); err != nil { + s.logger.ErrorContext(r.Context(), "failed to store device request", "err", err) s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError) return } @@ -125,15 +129,15 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) { }, } - if err := s.storage.CreateDeviceToken(deviceToken); err != nil { - s.logger.Errorf("Failed to store device token %v", err) + if err := s.storage.CreateDeviceToken(ctx, deviceToken); err != nil { + s.logger.ErrorContext(r.Context(), "failed to store device token", "err", err) s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError) return } u, err := url.Parse(s.issuerURL.String()) if err != nil { - s.logger.Errorf("Could not parse issuer URL %v", err) + s.logger.ErrorContext(r.Context(), "could not parse issuer URL", "err", err) s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError) return } @@ -174,14 +178,14 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleDeviceTokenDeprecated(w http.ResponseWriter, r *http.Request) { - log.Deprecated(s.logger, `The /device/token endpoint was called. It will be removed, use /token instead.`) + s.logger.Warn(`the /device/token endpoint was called. It will be removed, use /token instead.`, "deprecated", true) w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodPost: err := r.ParseForm() if err != nil { - s.logger.Warnf("Could not parse Device Token Request body: %v", err) + s.logger.Warn("could not parse Device Token Request body", "err", err) s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest) return } @@ -199,6 +203,7 @@ func (s *Server) handleDeviceTokenDeprecated(w http.ResponseWriter, r *http.Requ } func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() deviceCode := r.Form.Get("device_code") if deviceCode == "" { s.tokenErrHelper(w, errInvalidRequest, "No device code received", http.StatusBadRequest) @@ -208,10 +213,10 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) { now := s.now() // Grab the device token, check validity - deviceToken, err := s.storage.GetDeviceToken(deviceCode) + deviceToken, err := s.storage.GetDeviceToken(ctx, deviceCode) if err != nil { if err != storage.ErrNotFound { - s.logger.Errorf("failed to get device code: %v", err) + s.logger.ErrorContext(r.Context(), "failed to get device code", "err", err) } s.tokenErrHelper(w, errInvalidRequest, "Invalid Device code.", http.StatusBadRequest) return @@ -240,15 +245,15 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) { return old, nil } // Update device token last request time in storage - if err := s.storage.UpdateDeviceToken(deviceCode, updater); err != nil { - s.logger.Errorf("failed to update device token: %v", err) + if err := s.storage.UpdateDeviceToken(ctx, deviceCode, updater); err != nil { + s.logger.ErrorContext(r.Context(), "failed to update device token", "err", err) s.renderError(r, w, http.StatusInternalServerError, "") return } if slowDown { s.tokenErrHelper(w, deviceTokenSlowDown, "", http.StatusBadRequest) } else { - s.tokenErrHelper(w, deviceTokenPending, "", http.StatusUnauthorized) + s.tokenErrHelper(w, deviceTokenPending, "", http.StatusBadRequest) } case deviceTokenComplete: codeChallengeFromStorage := deviceToken.PKCE.CodeChallenge @@ -258,7 +263,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) { case providedCodeVerifier != "" && codeChallengeFromStorage != "": calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, deviceToken.PKCE.CodeChallengeMethod) if err != nil { - s.logger.Error(err) + s.logger.ErrorContext(r.Context(), "failed to calculate code challenge", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) return } @@ -280,6 +285,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() switch r.Method { case http.MethodGet: userCode := r.FormValue("state") @@ -292,17 +298,19 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) { // Authorization redirect callback from OAuth2 auth flow. if errMsg := r.FormValue("error"); errMsg != "" { - // escape the message to prevent cross-site scripting - msg := html.EscapeString(errMsg + ": " + r.FormValue("error_description")) - http.Error(w, msg, http.StatusBadRequest) + // Log the error details but don't expose them to the user + s.logger.ErrorContext(r.Context(), "OAuth2 authorization error", + "error", errMsg, + "error_description", r.FormValue("error_description")) + s.renderError(r, w, http.StatusBadRequest, "Authorization failed. Please try again.") return } - authCode, err := s.storage.GetAuthCode(code) + authCode, err := s.storage.GetAuthCode(ctx, code) if err != nil || s.now().After(authCode.Expiry) { errCode := http.StatusBadRequest if err != nil && err != storage.ErrNotFound { - s.logger.Errorf("failed to get auth code: %v", err) + s.logger.ErrorContext(r.Context(), "failed to get auth code", "err", err) errCode = http.StatusInternalServerError } s.renderError(r, w, errCode, "Invalid or expired auth code.") @@ -310,21 +318,21 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) { } // Grab the device request from storage - deviceReq, err := s.storage.GetDeviceRequest(userCode) + deviceReq, err := s.storage.GetDeviceRequest(ctx, userCode) if err != nil || s.now().After(deviceReq.Expiry) { errCode := http.StatusBadRequest if err != nil && err != storage.ErrNotFound { - s.logger.Errorf("failed to get device code: %v", err) + s.logger.ErrorContext(r.Context(), "failed to get device code", "err", err) errCode = http.StatusInternalServerError } s.renderError(r, w, errCode, "Invalid or expired user code.") return } - client, err := s.storage.GetClient(deviceReq.ClientID) + client, err := s.storage.GetClient(ctx, deviceReq.ClientID) if err != nil { if err != storage.ErrNotFound { - s.logger.Errorf("failed to get client: %v", err) + s.logger.ErrorContext(r.Context(), "failed to get client", "err", err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) } else { s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized) @@ -336,19 +344,19 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) { return } - resp, err := s.exchangeAuthCode(w, authCode, client) + resp, err := s.exchangeAuthCode(ctx, w, authCode, client) if err != nil { - s.logger.Errorf("Could not exchange auth code for client %q: %v", deviceReq.ClientID, err) + s.logger.ErrorContext(r.Context(), "could not exchange auth code for clien", "client_id", deviceReq.ClientID, "err", err) s.renderError(r, w, http.StatusInternalServerError, "Failed to exchange auth code.") return } // Grab the device token from storage - old, err := s.storage.GetDeviceToken(deviceReq.DeviceCode) + old, err := s.storage.GetDeviceToken(ctx, deviceReq.DeviceCode) if err != nil || s.now().After(old.Expiry) { errCode := http.StatusBadRequest if err != nil && err != storage.ErrNotFound { - s.logger.Errorf("failed to get device token: %v", err) + s.logger.ErrorContext(r.Context(), "failed to get device token", "err", err) errCode = http.StatusInternalServerError } s.renderError(r, w, errCode, "Invalid or expired device code.") @@ -361,7 +369,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) { } respStr, err := json.MarshalIndent(resp, "", " ") if err != nil { - s.logger.Errorf("failed to marshal device token response: %v", err) + s.logger.ErrorContext(r.Context(), "failed to marshal device token response", "err", err) s.renderError(r, w, http.StatusInternalServerError, "") return old, err } @@ -372,29 +380,31 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) { } // Update refresh token in the storage, store the token and mark as complete - if err := s.storage.UpdateDeviceToken(deviceReq.DeviceCode, updater); err != nil { - s.logger.Errorf("failed to update device token: %v", err) + if err := s.storage.UpdateDeviceToken(ctx, deviceReq.DeviceCode, updater); err != nil { + s.logger.ErrorContext(r.Context(), "failed to update device token", "err", err) s.renderError(r, w, http.StatusBadRequest, "") return } if err := s.templates.deviceSuccess(r, w, client.Name); err != nil { - s.logger.Errorf("Server template error: %v", err) + s.logger.ErrorContext(r.Context(), "Server template error", "err", err) s.renderError(r, w, http.StatusNotFound, "Page not found") } default: - http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest) + s.logger.ErrorContext(r.Context(), "unsupported method in device callback", "method", r.Method) + s.renderError(r, w, http.StatusBadRequest, ErrMsgMethodNotAllowed) return } } func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() switch r.Method { case http.MethodPost: err := r.ParseForm() if err != nil { - s.logger.Warnf("Could not parse user code verification request body : %v", err) + s.logger.Warn("could not parse user code verification request body", "err", err) s.renderError(r, w, http.StatusBadRequest, "") return } @@ -408,20 +418,20 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) { userCode = strings.ToUpper(userCode) // Find the user code in the available requests - deviceRequest, err := s.storage.GetDeviceRequest(userCode) + deviceRequest, err := s.storage.GetDeviceRequest(ctx, userCode) if err != nil || s.now().After(deviceRequest.Expiry) { if err != nil && err != storage.ErrNotFound { - s.logger.Errorf("failed to get device request: %v", err) + s.logger.ErrorContext(r.Context(), "failed to get device request", "err", err) } if err := s.templates.device(r, w, s.getDeviceVerificationURI(), userCode, true); err != nil { - s.logger.Errorf("Server template error: %v", err) + s.logger.ErrorContext(r.Context(), "Server template error", "err", err) s.renderError(r, w, http.StatusNotFound, "Page not found") } return } // Redirect to Dex Auth Endpoint - authURL := path.Join(s.issuerURL.Path, "/auth") + authURL := s.absURL("/auth") u, err := url.Parse(authURL) if err != nil { s.renderError(r, w, http.StatusInternalServerError, "Invalid auth URI.") @@ -432,7 +442,7 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) { q.Set("client_secret", deviceRequest.ClientSecret) q.Set("state", deviceRequest.UserCode) q.Set("response_type", "code") - q.Set("redirect_uri", "/device/callback") + q.Set("redirect_uri", s.absPath(deviceCallbackURI)) q.Set("scope", strings.Join(deviceRequest.Scopes, " ")) u.RawQuery = q.Encode() diff --git a/server/deviceflowhandlers_test.go b/server/deviceflowhandlers_test.go index 9a9f28584e..1cbd60f716 100644 --- a/server/deviceflowhandlers_test.go +++ b/server/deviceflowhandlers_test.go @@ -2,7 +2,6 @@ package server import ( "bytes" - "context" "encoding/json" "io" "net/http" @@ -20,10 +19,8 @@ func TestDeviceVerificationURI(t *testing.T) { t0 := time.Now() now := func() time.Time { return t0 } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // Setup a dex server. - httpServer, s := newTestServer(ctx, t, func(c *Config) { + httpServer, s := newTestServer(t, func(c *Config) { c.Issuer += "/non-root-path" c.Now = now }) @@ -90,14 +87,19 @@ func TestHandleDeviceCode(t *testing.T) { expectedResponseCode: http.StatusBadRequest, expectedContentType: "application/json", }, + { + testName: "New Code without scope", + clientID: "test", + requestType: "POST", + scopes: []string{}, + expectedResponseCode: http.StatusOK, + expectedContentType: "application/json", + }, } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // Setup a dex server. - httpServer, s := newTestServer(ctx, t, func(c *Config) { + httpServer, s := newTestServer(t, func(c *Config) { c.Issuer += "/non-root-path" c.Now = now }) @@ -220,8 +222,9 @@ func TestDeviceCallback(t *testing.T) { code: "somecode", error: "Error Condition", }, - expectedResponseCode: http.StatusBadRequest, - expectedServerResponse: "Error Condition: \n", + expectedResponseCode: http.StatusBadRequest, + // Note: Error details should NOT be displayed to user anymore. + // Instead, a safe generic message is shown. }, { testName: "Expired Auth Code", @@ -350,31 +353,31 @@ func TestDeviceCallback(t *testing.T) { code: "somecode", error: "", }, - expectedResponseCode: http.StatusBadRequest, - expectedServerResponse: "<script>console.log(window);</script>: \n", + expectedResponseCode: http.StatusBadRequest, + // Note: XSS data should NOT be displayed to user anymore. + // Instead, a safe generic message is shown. }, } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() // Setup a dex server. - httpServer, s := newTestServer(ctx, t, func(c *Config) { - // c.Issuer = c.Issuer + "/non-root-path" + httpServer, s := newTestServer(t, func(c *Config) { + c.Issuer = c.Issuer + "/non-root-path" c.Now = now }) defer httpServer.Close() - if err := s.storage.CreateAuthCode(tc.testAuthCode); err != nil { + if err := s.storage.CreateAuthCode(ctx, tc.testAuthCode); err != nil { t.Fatalf("failed to create auth code: %v", err) } - if err := s.storage.CreateDeviceRequest(tc.testDeviceRequest); err != nil { + if err := s.storage.CreateDeviceRequest(ctx, tc.testDeviceRequest); err != nil { t.Fatalf("failed to create device request: %v", err) } - if err := s.storage.CreateDeviceToken(tc.testDeviceToken); err != nil { + if err := s.storage.CreateDeviceToken(ctx, tc.testDeviceToken); err != nil { t.Fatalf("failed to create device token: %v", err) } @@ -383,7 +386,7 @@ func TestDeviceCallback(t *testing.T) { Secret: "", RedirectURIs: []string{deviceCallbackURI}, } - if err := s.storage.CreateClient(client); err != nil { + if err := s.storage.CreateClient(ctx, client); err != nil { t.Fatalf("failed to create client: %v", err) } @@ -412,6 +415,29 @@ func TestDeviceCallback(t *testing.T) { t.Errorf("%s: Unexpected Response. Expected %q got %q", tc.testName, tc.expectedServerResponse, result) } } + + // Special check for error message safety tests + if tc.testName == "Prevent cross-site scripting" || tc.testName == "Error During Authorization" { + result, _ := io.ReadAll(rr.Body) + responseBody := string(result) + + // Error details should NOT be present in the response (for security) + if tc.testName == "Prevent cross-site scripting" { + if strings.Contains(responseBody, " + {{ template "footer.html" . }} diff --git a/web/templates/totp_verify.html b/web/templates/totp_verify.html new file mode 100644 index 0000000000..2e00c3263e --- /dev/null +++ b/web/templates/totp_verify.html @@ -0,0 +1,36 @@ +{{ template "header.html" . }} + +
+

Two-factor authentication

+ {{ if not (eq .QRCode "") }} +

Scan the QR code below using your authenticator app, then enter the code.

+
+ QR code +
+ {{ else }} +

Enter the code from your authenticator app.

+

{{ .Issuer }}

+ {{ end }} +
+
+
+ +
+ +
+ + {{ if .Invalid }} +
+ Invalid code. Please try again. +
+ {{ end }} + + +
+
+ +{{ template "footer.html" . }} diff --git a/web/themes/dark/styles.css b/web/themes/dark/styles.css index edf30412fa..d6cb393c83 100644 --- a/web/themes/dark/styles.css +++ b/web/themes/dark/styles.css @@ -1,122 +1,137 @@ .theme-body { - background-color: #0f1218; - color: #c8d1d9; - font-family: 'Source Sans Pro', Helvetica, sans-serif; + background-color: #131519; + color: #b8bcc4; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } .theme-navbar { - background-color: #161b22; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - color: #161B2B; + background-color: #1a1d23; + border-bottom: 1px solid #2a2d35; + color: #b8bcc4; font-size: 13px; - font-weight: 100; - height: 46px; + font-weight: 400; + height: 52px; overflow: hidden; - padding: 0 10px; + padding: 0 16px; } .theme-navbar__logo-wrap { display: inline-block; height: 100%; overflow: hidden; - padding: 10px 15px; + padding: 12px 15px; width: 300px; } .theme-navbar__logo { height: 100%; - max-height: 25px; + max-height: 26px; } .theme-heading { + color: #dcdfe5; font-size: 20px; - font-weight: 500; + font-weight: 600; + margin-bottom: 16px; margin-top: 0; - color: #c8d1d9; } .theme-panel { - background-color: #161b22; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - padding: 30px; + background-color: #1a1d23; + border: 1px solid #2a2d35; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + padding: 32px; } .theme-btn-provider { - background-color: #1e242d; - color: #c8d1d9; - border: 1px solid #30373c; - min-width: 250px; + background-color: #22252c; + border: 1px solid #33363e; + color: #b8bcc4; + min-width: 260px; } .theme-btn-provider:hover { - background-color: #212731; - color: #ffffff; + background-color: #2a2d35; + border-color: #3e414a; + color: #dcdfe5; } .theme-btn--primary { - background-color: #1e242d; + background-color: #3d3f47; border: none; - color: #c8d1d9; + color: #e8eaed; min-width: 200px; - padding: 6px 12px; + padding: 8px 16px; } .theme-btn--primary:hover { - background-color: #212731; - color: #e9e9e9; + background-color: #4a4c55; + color: #fff; } .theme-btn--success { - background-color: #1891bb; - color: #e9e9e9; - width: 250px; + background-color: #2d7d9a; + color: #e8eaed; + width: 260px; } .theme-btn--success:hover { - background-color: #1da5d4; + background-color: #358fae; } .theme-form-row { display: block; - margin: 20px auto; + margin: 16px auto; } .theme-form-input { + background-color: #131519; + border: 1px solid #33363e; + border-radius: 8px; + color: #b8bcc4; display: block; - height: 36px; - padding: 6px 12px; font-size: 14px; - line-height: 1.42857143; - border: 1px solid #515559; - border-radius: 4px; - color: #c8d1d9; - background-color: #0f1218; - box-shadow: inset 0 1px 1px rgb(27, 40, 46); - width: 250px; + height: 40px; + line-height: 1.5; margin: auto; + padding: 8px 12px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + width: 260px; } .theme-form-input:focus, .theme-form-input:active { + border-color: #5a9bb5; + box-shadow: 0 0 0 3px rgba(90, 155, 181, 0.15); + color: #dcdfe5; outline: none; - border-color: #f8f9f9; - color: #c8d1d9; } .theme-form-label { - width: 250px; + color: #b8bcc4; + font-size: 14px; + font-weight: 500; margin: 4px auto; - text-align: left; position: relative; - font-size: 13px; - font-weight: 600; - color: #c8d1d9; + text-align: left; + width: 260px; } .theme-link-back { - margin-top: 4px; + margin-top: 8px; +} + +.theme-remember-me { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; + width: 260px; + margin: 4px auto; } .dex-container { - color: #c8d1d9; + color: #b8bcc4; } diff --git a/web/themes/light/styles.css b/web/themes/light/styles.css index 2d92057119..37001aedd5 100644 --- a/web/themes/light/styles.css +++ b/web/themes/light/styles.css @@ -1,113 +1,129 @@ .theme-body { - background-color: #efefef; - color: #333; - font-family: 'Source Sans Pro', Helvetica, sans-serif; + background-color: #f4f5f7; + color: #1a1a1a; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } .theme-navbar { background-color: #fff; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); - color: #333; + border-bottom: 1px solid #e1e4e8; + color: #1a1a1a; font-size: 13px; - font-weight: 100; - height: 46px; + font-weight: 400; + height: 52px; overflow: hidden; - padding: 0 10px; + padding: 0 16px; } .theme-navbar__logo-wrap { display: inline-block; height: 100%; overflow: hidden; - padding: 10px 15px; + padding: 12px 15px; width: 300px; } .theme-navbar__logo { height: 100%; - max-height: 25px; + max-height: 26px; } .theme-heading { font-size: 20px; - font-weight: 500; - margin-bottom: 10px; + font-weight: 600; + margin-bottom: 16px; margin-top: 0; } .theme-panel { background-color: #fff; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - padding: 30px; + border: 1px solid #e1e4e8; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06); + padding: 32px; } .theme-btn-provider { background-color: #fff; - color: #333; - min-width: 250px; + border: 1px solid #d0d5dd; + color: #1a1a1a; + min-width: 260px; } .theme-btn-provider:hover { - color: #999; + background-color: #f9fafb; + border-color: #b0b5bd; + color: #1a1a1a; } .theme-btn--primary { - background-color: #333; + background-color: #1a1a1a; border: none; color: #fff; min-width: 200px; - padding: 6px 12px; + padding: 8px 16px; } .theme-btn--primary:hover { - background-color: #666; + background-color: #333; color: #fff; } .theme-btn--success { - background-color: #2FC98E; + background-color: #16a34a; color: #fff; - width: 250px; + width: 260px; } .theme-btn--success:hover { - background-color: #49E3A8; + background-color: #15803d; } .theme-form-row { display: block; - margin: 20px auto; + margin: 16px auto; } .theme-form-input { - border-radius: 4px; - border: 1px solid #CCC; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - color: #666; + border-radius: 8px; + border: 1px solid #d0d5dd; + color: #1a1a1a; display: block; font-size: 14px; - height: 36px; - line-height: 1.42857143; + height: 40px; + line-height: 1.5; margin: auto; - padding: 6px 12px; - width: 250px; + padding: 8px 12px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + width: 260px; } .theme-form-input:focus, .theme-form-input:active { - border-color: #66AFE9; + border-color: #4A90D9; + box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15); outline: none; } .theme-form-label { - font-size: 13px; - font-weight: 600; + font-size: 14px; + font-weight: 500; margin: 4px auto; position: relative; text-align: left; - width: 250px; + width: 260px; } .theme-link-back { - margin-top: 4px; + margin-top: 8px; +} + +.theme-remember-me { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; + width: 260px; + margin: 4px auto; } diff --git a/web/web.go b/web/web.go index c5ff751490..0c7e9873a7 100644 --- a/web/web.go +++ b/web/web.go @@ -5,7 +5,7 @@ import ( "io/fs" ) -//go:embed static/* templates/* themes/* +//go:embed static/* templates/* themes/* robots.txt var files embed.FS // FS returns a filesystem with the default web assets.