mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-02-22 22:04:21 +00:00
Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4badb3961 | ||
|
|
1e32530d7b | ||
|
|
0ab8e14e4a | ||
|
|
f1109afa79 | ||
|
|
22be97eeda | ||
|
|
d23f3c9428 | ||
|
|
c3e037b24e | ||
|
|
62afb34877 | ||
|
|
fd5ed2e5da | ||
|
|
ca82609d1a | ||
|
|
d73093a694 | ||
|
|
85560299e0 | ||
|
|
b42b1c1110 | ||
|
|
84733405d0 | ||
|
|
48c8fabb5c | ||
|
|
1f51159d22 | ||
|
|
5f679059d5 | ||
|
|
8afbb4eb4f | ||
|
|
c9b4598fa0 | ||
|
|
ef33846d7d | ||
|
|
8b4ed86071 | ||
|
|
8b74dd824b | ||
|
|
727035a2dc | ||
|
|
fc82fde585 | ||
|
|
f352db8072 | ||
|
|
22a66c1192 | ||
|
|
8e8af51955 | ||
|
|
d5759c9951 | ||
|
|
bf99348faf | ||
|
|
2789af79f6 | ||
|
|
71cae75758 | ||
|
|
90db3ad51b | ||
|
|
32c9aa5087 | ||
|
|
f17f7c0c6a | ||
|
|
54c5bcc9a1 | ||
|
|
58ab57201f | ||
|
|
f4829178b3 | ||
|
|
295f013580 | ||
|
|
d8e1521457 | ||
|
|
e150111b27 | ||
|
|
9e27c28b39 | ||
|
|
ac4bc02817 | ||
|
|
708164b878 | ||
|
|
e30cf6e51a | ||
|
|
ee30b78117 | ||
|
|
64aba7e703 | ||
|
|
c6c3a80a86 | ||
|
|
a7af63ca3a | ||
|
|
ae72d30cec | ||
|
|
fec59eb1bf | ||
|
|
f6273b0604 | ||
|
|
deb699a84a | ||
|
|
cac45fd999 | ||
|
|
0bdd1bc68f | ||
|
|
4605846499 | ||
|
|
4670890a82 | ||
|
|
d204b46c18 | ||
|
|
b3208f0ca6 | ||
|
|
be7bf9c193 | ||
|
|
2bba39d723 | ||
|
|
dc567d0d1f | ||
|
|
143837c136 | ||
|
|
11eb18d348 | ||
|
|
d5cf5b91d6 | ||
|
|
48b58e2fad | ||
|
|
7ef3d42e01 | ||
|
|
33461ddc14 | ||
|
|
238c9e6743 | ||
|
|
25cda4f3e6 | ||
|
|
c71703e4db | ||
|
|
5527566a36 | ||
|
|
b6580b303a | ||
|
|
405a27ba90 | ||
|
|
43f66032a9 | ||
|
|
91a1fec5cf | ||
|
|
d0b37a7c90 | ||
|
|
5029495fdb | ||
|
|
ac6ff1a03c | ||
|
|
95ee9f0b00 | ||
|
|
9fba8d2203 | ||
|
|
428f389c7d | ||
|
|
71d4e05fb6 | ||
|
|
1904f8ddc3 | ||
|
|
6b801056b5 | ||
|
|
674cd4a88c | ||
|
|
7217cf4892 | ||
|
|
40c931bdc5 | ||
|
|
2823d4d1e3 | ||
|
|
6d83ecb420 | ||
|
|
c27eb17f23 | ||
|
|
58363bca2c | ||
|
|
3bf39797bb | ||
|
|
3a840cee76 | ||
|
|
04e9897d51 | ||
|
|
ff9095f9c4 | ||
|
|
2e18c88e33 | ||
|
|
9f91c6c884 | ||
|
|
59fd1997f4 | ||
|
|
74f3ce5dcd | ||
|
|
d403c8b44b | ||
|
|
e130da6daa | ||
|
|
2f7dbed321 | ||
|
|
709c10227f | ||
|
|
f0d120a6ca | ||
|
|
1f5480cd5c | ||
|
|
f1e63c55d4 | ||
|
|
562951b77a | ||
|
|
dbd2cb4563 | ||
|
|
1c746feafe | ||
|
|
49683975ab | ||
|
|
4be26fc1a6 | ||
|
|
f590a3a88b | ||
|
|
e26486bd41 | ||
|
|
5c2e890ecd | ||
|
|
715cf7748a | ||
|
|
e0456b4485 | ||
|
|
629bf61655 | ||
|
|
738e6aa3cc | ||
|
|
76dc39ac2d | ||
|
|
43fee6bb94 | ||
|
|
8026729c43 | ||
|
|
d19d63ad7d | ||
|
|
a6141e911c | ||
|
|
5ff2be973c | ||
|
|
73201ba575 | ||
|
|
125d891cd5 | ||
|
|
682a47f739 | ||
|
|
594e47efdf | ||
|
|
f09a45382e | ||
|
|
d78b845575 | ||
|
|
1929b47dda | ||
|
|
3efa7bdcc2 | ||
|
|
6a9eb87c35 | ||
|
|
3eba3e07c6 | ||
|
|
9f06869f76 | ||
|
|
3f41261580 | ||
|
|
551249fb69 | ||
|
|
1a610022cf | ||
|
|
524ff21b7f | ||
|
|
913c140be8 |
@@ -1,12 +1,23 @@
|
||||
./.*
|
||||
./*.md
|
||||
./*.yaml
|
||||
./apis
|
||||
./deploy
|
||||
./Dockerfile
|
||||
./generated/1.1*
|
||||
./internal/mocks
|
||||
./LICENSE
|
||||
./site/
|
||||
./test
|
||||
**/*_test.go
|
||||
# This is effectively a copy of the .gitignore file.
|
||||
# The whole git repo, including the .git directory, should get copied into the Docker build context,
|
||||
# to enable the use of hack/get-ldflags.sh inside the Dockerfile.
|
||||
# When you change the .gitignore file, please consider also changing this file.
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# GoLand
|
||||
.idea
|
||||
|
||||
# MacOS Desktop Services Store
|
||||
.DS_Store
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
# When you change this file, please consider also changing the .dockerignore file.
|
||||
# See comments at the top of .dockerignore for more information.
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
@@ -11,9 +14,6 @@
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# GoLand
|
||||
.idea
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.16.5 as build-env
|
||||
FROM golang:1.16.6 as build-env
|
||||
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
@@ -27,11 +27,11 @@ RUN \
|
||||
./cmd/local-user-authenticator/...
|
||||
|
||||
# Use a Debian slim image to grab a reasonable default CA bundle.
|
||||
FROM debian:10.9-slim AS get-ca-bundle-env
|
||||
FROM debian:10.10-slim AS get-ca-bundle-env
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* /var/cache/debconf/*
|
||||
|
||||
# Use a runtime image based on Debian slim.
|
||||
FROM debian:10.9-slim
|
||||
FROM debian:10.10-slim
|
||||
COPY --from=get-ca-bundle-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Copy the binaries from the build-env stage.
|
||||
|
||||
@@ -4,16 +4,21 @@ This is the current list of maintainers for the Pinniped project.
|
||||
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
| --------------- | --------- | ----------- |
|
||||
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) | [VMware](https://www.github.com/vmware/) |
|
||||
| Margo Crawford | [margocrawf](https://github.com/margocrawf) | [VMware](https://www.github.com/vmware/) |
|
||||
| Matt Moyer | [mattmoyer](https://github.com/mattmoyer) | [VMware](https://www.github.com/vmware/) |
|
||||
| Mo Khan | [enj](https://github.com/enj) | [VMware](https://www.github.com/vmware/) |
|
||||
| Pablo Schuhmacher | [pabloschuhmacher](https://github.com/pabloschuhmacher) | [VMware](https://www.github.com/vmware/) |
|
||||
| Anjali Telang | [anjaltelang](https://github.com/anjaltelang) | [VMware](https://www.github.com/vmware/) |
|
||||
| Ryan Richard | [cfryanr](https://github.com/cfryanr) | [VMware](https://www.github.com/vmware/) |
|
||||
|
||||
## Emeritus Maintainers
|
||||
|
||||
* Andrew Keesler, [ankeesler](https://github.com/ankeesler)
|
||||
* Pablo Schuhmacher, [pabloschuhmacher](https://github.com/pabloschuhmacher)
|
||||
|
||||
## Pinniped Contributors & Stakeholders
|
||||
|
||||
| Feature Area | Lead |
|
||||
| ----------------------------- | :---------------------: |
|
||||
| Technical Lead | Matt Moyer (mattmoyer) |
|
||||
| Product Management | Pablo Schuhmacher (pabloschuhmacher) |
|
||||
| Product Management | Anjali Telang (anjaltelang) |
|
||||
| Community Management | Nanci Lancaster (microwavables) |
|
||||
|
||||
12
ROADMAP.md
12
ROADMAP.md
@@ -33,12 +33,15 @@ The following table includes the current roadmap for Pinniped. If you have any q
|
||||
|
||||
|
||||
|
||||
Last Updated: June 2021
|
||||
Last Updated: July 2021
|
||||
Theme|Description|Timeline|
|
||||
|--|--|--|
|
||||
|Remote OIDC login support|Add support for logging in from remote hosts without web browsers in the Pinniped CLI and Supervisor|Jun 2021|
|
||||
|AD Support|Extends upstream IDP protocols|Jun 2021|
|
||||
|Wider Concierge cluster support|Support for more cluster types in the Concierge|Jul 2021|
|
||||
|Remote OIDC login support|Add support for logging in from remote hosts without web browsers in the Pinniped CLI and Supervisor|Jul 2021|
|
||||
|Active Directory Support|Extends upstream IDP protocols|Aug 2021|
|
||||
|Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Sept 2021|
|
||||
|Wider Concierge cluster support|Support for more cluster types in the Concierge|Sept 2021|
|
||||
|Identity transforms|Support prefixing, filtering, or performing coarse-grained checks on upstream users and groups|Exploring/Ongoing|
|
||||
|Extended IDP support|Support more types of identity providers on the Supervisor|Exploring/Ongoing|
|
||||
|Improved Documentation|Reorganizing and improving Pinniped docs; new how-to guides and tutorials|Exploring/Ongoing|
|
||||
|Improving Security Posture|Offer the best security posture for Kubernetes cluster authentication|Exploring/Ongoing|
|
||||
|Improve our CI/CD systems|Upgrade tests; make Kind more efficient and reliable for CI ; Windows tests; performance tests; scale tests; soak tests|Exploring/Ongoing|
|
||||
@@ -46,5 +49,6 @@ Theme|Description|Timeline|
|
||||
|Telemetry|Adding some useful phone home metrics as well as some vanity metrics|Exploring/Ongoing|
|
||||
|Observability|Expose Pinniped metrics through Prometheus Integration|Exploring/Ongoing|
|
||||
|Device Code Flow|Add support for OAuth 2.0 Device Authorization Grant in the Pinniped CLI and Supervisor|Exploring/Ongoing|
|
||||
|Supervisor with New Clients|Enable registering new clients with Supervisor|Exploring/Ongoing|
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ type ImpersonationProxySpec struct {
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
@@ -61,6 +61,7 @@ type getKubeconfigOIDCParams struct {
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
skipListen bool
|
||||
sessionCachePath string
|
||||
debugSessionCache bool
|
||||
caBundle caBundleFlag
|
||||
@@ -146,6 +147,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login")
|
||||
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
|
||||
f.BoolVar(&flags.oidc.skipListen, "oidc-skip-listen", false, "During OpenID Connect login, skip starting a localhost callback listener (manual copy/paste flow only)")
|
||||
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
|
||||
f.Var(&flags.oidc.caBundle, "oidc-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||
f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache")
|
||||
@@ -161,6 +163,9 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
f.StringVar(&flags.credentialCachePath, "credential-cache", "", "Path to cluster-specific credentials cache")
|
||||
mustMarkHidden(cmd, "oidc-debug-session-cache")
|
||||
|
||||
// --oidc-skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
|
||||
mustMarkHidden(cmd, "oidc-skip-listen")
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
mustMarkHidden(cmd, "concierge-namespace")
|
||||
|
||||
@@ -317,6 +322,9 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
|
||||
if flags.oidc.skipBrowser {
|
||||
execConfig.Args = append(execConfig.Args, "--skip-browser")
|
||||
}
|
||||
if flags.oidc.skipListen {
|
||||
execConfig.Args = append(execConfig.Args, "--skip-listen")
|
||||
}
|
||||
if flags.oidc.listenPort != 0 {
|
||||
execConfig.Args = append(execConfig.Args, "--listen-port="+strconv.Itoa(int(flags.oidc.listenPort)))
|
||||
}
|
||||
|
||||
@@ -1352,6 +1352,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
"--concierge-ca-bundle", testConciergeCABundlePath,
|
||||
"--oidc-issuer", issuerURL,
|
||||
"--oidc-skip-browser",
|
||||
"--oidc-skip-listen",
|
||||
"--oidc-listen-port", "1234",
|
||||
"--oidc-ca-bundle", f.Name(),
|
||||
"--oidc-session-cache", "/path/to/cache/dir/sessions.yaml",
|
||||
@@ -1405,6 +1406,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --client-id=pinniped-cli
|
||||
- --scopes=offline_access,openid,pinniped:request-audience
|
||||
- --skip-browser
|
||||
- --skip-listen
|
||||
- --listen-port=1234
|
||||
- --ca-bundle-data=%s
|
||||
- --session-cache=/path/to/cache/dir/sessions.yaml
|
||||
|
||||
@@ -59,6 +59,7 @@ type oidcLoginFlags struct {
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
skipListen bool
|
||||
sessionCachePath string
|
||||
caBundlePaths []string
|
||||
caBundleData []string
|
||||
@@ -91,6 +92,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
cmd.Flags().Uint16Var(&flags.listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||
cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OIDC scopes to request during login")
|
||||
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
||||
cmd.Flags().BoolVar(&flags.skipListen, "skip-listen", false, "Skip starting a localhost callback listener (manual copy/paste flow only)")
|
||||
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
||||
cmd.Flags().StringSliceVar(&flags.caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||
cmd.Flags().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)")
|
||||
@@ -107,6 +109,8 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
cmd.Flags().StringVar(&flags.upstreamIdentityProviderName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor")
|
||||
cmd.Flags().StringVar(&flags.upstreamIdentityProviderType, "upstream-identity-provider-type", "oidc", "The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap')")
|
||||
|
||||
// --skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
|
||||
mustMarkHidden(cmd, "skip-listen")
|
||||
mustMarkHidden(cmd, "debug-session-cache")
|
||||
mustMarkRequired(cmd, "issuer")
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runOIDCLogin(cmd, deps, flags) }
|
||||
@@ -182,12 +186,14 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
}
|
||||
}
|
||||
|
||||
// --skip-browser replaces the default "browser open" function with one that prints to stderr.
|
||||
// --skip-browser skips opening the browser.
|
||||
if flags.skipBrowser {
|
||||
opts = append(opts, oidcclient.WithBrowserOpen(func(url string) error {
|
||||
cmd.PrintErr("Please log in: ", url, "\n")
|
||||
return nil
|
||||
}))
|
||||
opts = append(opts, oidcclient.WithSkipBrowserOpen())
|
||||
}
|
||||
|
||||
// --skip-listen skips starting the localhost callback listener.
|
||||
if flags.skipListen {
|
||||
opts = append(opts, oidcclient.WithSkipListen())
|
||||
}
|
||||
|
||||
if len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
|
||||
|
||||
@@ -226,6 +226,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--skip-browser",
|
||||
"--skip-listen",
|
||||
"--listen-port", "1234",
|
||||
"--debug-session-cache",
|
||||
"--request-audience", "cluster-1234",
|
||||
@@ -242,7 +243,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true"},
|
||||
wantOptionsCount: 10,
|
||||
wantOptionsCount: 11,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n",
|
||||
wantLogs: []string{
|
||||
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
|
||||
|
||||
@@ -47,7 +47,7 @@ spec:
|
||||
description: "ExternalEndpoint describes the HTTPS endpoint where
|
||||
the proxy will be exposed. If not set, the proxy will be served
|
||||
using the external name of the LoadBalancer service or the cluster
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.type
|
||||
is \"None\"."
|
||||
type: string
|
||||
mode:
|
||||
|
||||
@@ -102,6 +102,15 @@ spec:
|
||||
protocol: TCP
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
env:
|
||||
#@ if data.values.https_proxy:
|
||||
- name: HTTPS_PROXY
|
||||
value: #@ data.values.https_proxy
|
||||
#@ end
|
||||
#@ if data.values.no_proxy:
|
||||
- name: NO_PROXY
|
||||
value: #@ data.values.no_proxy
|
||||
#@ end
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
|
||||
@@ -65,3 +65,11 @@ run_as_group: 1001 #! run_as_group specifies the group ID that will own the proc
|
||||
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
|
||||
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
|
||||
api_group_suffix: pinniped.dev
|
||||
|
||||
#! Set the standard golang HTTPS_PROXY and NO_PROXY environment variables on the Supervisor containers.
|
||||
#! These will be used when the Supervisor makes backend-to-backend calls to upstream identity providers using HTTPS,
|
||||
#! e.g. when the Supervisor fetches discovery documents, JWKS keys, and tokens from an upstream OIDC Provider.
|
||||
#! The Supervisor never makes insecure HTTP calls, so there is no reason to set HTTP_PROXY.
|
||||
#! Optional.
|
||||
https_proxy: #! e.g. http://proxy.example.com
|
||||
no_proxy: #! e.g. 127.0.0.1
|
||||
|
||||
2
generated/1.17/README.adoc
generated
2
generated/1.17/README.adoc
generated
@@ -411,7 +411,7 @@ ImpersonationProxyServiceSpec describes how the Concierge should provision a Ser
|
||||
| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running.
|
||||
| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients.
|
||||
| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ type ImpersonationProxySpec struct {
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
@@ -47,7 +47,7 @@ spec:
|
||||
description: "ExternalEndpoint describes the HTTPS endpoint where
|
||||
the proxy will be exposed. If not set, the proxy will be served
|
||||
using the external name of the LoadBalancer service or the cluster
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.type
|
||||
is \"None\"."
|
||||
type: string
|
||||
mode:
|
||||
|
||||
2
generated/1.18/README.adoc
generated
2
generated/1.18/README.adoc
generated
@@ -411,7 +411,7 @@ ImpersonationProxyServiceSpec describes how the Concierge should provision a Ser
|
||||
| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running.
|
||||
| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients.
|
||||
| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ type ImpersonationProxySpec struct {
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
@@ -47,7 +47,7 @@ spec:
|
||||
description: "ExternalEndpoint describes the HTTPS endpoint where
|
||||
the proxy will be exposed. If not set, the proxy will be served
|
||||
using the external name of the LoadBalancer service or the cluster
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.type
|
||||
is \"None\"."
|
||||
type: string
|
||||
mode:
|
||||
|
||||
2
generated/1.19/README.adoc
generated
2
generated/1.19/README.adoc
generated
@@ -411,7 +411,7 @@ ImpersonationProxyServiceSpec describes how the Concierge should provision a Ser
|
||||
| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running.
|
||||
| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients.
|
||||
| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ type ImpersonationProxySpec struct {
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
@@ -47,7 +47,7 @@ spec:
|
||||
description: "ExternalEndpoint describes the HTTPS endpoint where
|
||||
the proxy will be exposed. If not set, the proxy will be served
|
||||
using the external name of the LoadBalancer service or the cluster
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.type
|
||||
is \"None\"."
|
||||
type: string
|
||||
mode:
|
||||
|
||||
2
generated/1.20/README.adoc
generated
2
generated/1.20/README.adoc
generated
@@ -411,7 +411,7 @@ ImpersonationProxyServiceSpec describes how the Concierge should provision a Ser
|
||||
| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running.
|
||||
| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients.
|
||||
| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ type ImpersonationProxySpec struct {
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
@@ -47,7 +47,7 @@ spec:
|
||||
description: "ExternalEndpoint describes the HTTPS endpoint where
|
||||
the proxy will be exposed. If not set, the proxy will be served
|
||||
using the external name of the LoadBalancer service or the cluster
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode
|
||||
service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.type
|
||||
is \"None\"."
|
||||
type: string
|
||||
mode:
|
||||
|
||||
@@ -96,7 +96,7 @@ type ImpersonationProxySpec struct {
|
||||
// ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will
|
||||
// be served using the external name of the LoadBalancer service or the cluster service DNS name.
|
||||
//
|
||||
// This field must be non-empty when spec.impersonationProxy.service.mode is "None".
|
||||
// This field must be non-empty when spec.impersonationProxy.service.type is "None".
|
||||
//
|
||||
// +optional
|
||||
ExternalEndpoint string `json:"externalEndpoint,omitempty"`
|
||||
|
||||
27
go.mod
27
go.mod
@@ -1,18 +1,18 @@
|
||||
module go.pinniped.dev
|
||||
|
||||
go 1.14
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||
github.com/coreos/go-oidc/v3 v3.0.0
|
||||
github.com/creack/pty v1.1.13
|
||||
github.com/creack/pty v1.1.14
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
github.com/go-logr/logr v0.4.0
|
||||
github.com/go-logr/stdr v0.4.0
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/gofrs/flock v0.8.0
|
||||
github.com/golang/mock v1.5.0
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
@@ -23,23 +23,24 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sclevine/agouti v3.0.0+incompatible
|
||||
github.com/sclevine/spec v1.4.0
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tdewolff/minify/v2 v2.9.20
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
k8s.io/api v0.21.1
|
||||
k8s.io/apimachinery v0.21.1
|
||||
k8s.io/apiserver v0.21.1
|
||||
k8s.io/client-go v0.21.1
|
||||
k8s.io/component-base v0.21.1
|
||||
k8s.io/api v0.21.3
|
||||
k8s.io/apimachinery v0.21.3
|
||||
k8s.io/apiserver v0.21.3
|
||||
k8s.io/client-go v0.21.3
|
||||
k8s.io/component-base v0.21.3
|
||||
k8s.io/gengo v0.0.0-20210203185629-de9496dff47b
|
||||
k8s.io/klog/v2 v2.9.0
|
||||
k8s.io/kube-aggregator v0.21.1
|
||||
k8s.io/klog/v2 v2.10.0
|
||||
k8s.io/kube-aggregator v0.21.3
|
||||
k8s.io/utils v0.0.0-20210521133846-da695404a2bc
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
262
go.sum
262
go.sum
@@ -9,20 +9,35 @@ cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg
|
||||
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 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0=
|
||||
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 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
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.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
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/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
@@ -71,6 +86,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
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/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@@ -89,6 +105,7 @@ 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
|
||||
@@ -101,6 +118,7 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
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=
|
||||
@@ -133,6 +151,7 @@ github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
@@ -143,8 +162,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.13 h1:rTPnd/xocYRjutMfqide2zle1u96upp1gm6eUHKi7us=
|
||||
github.com/creack/pty v1.1.13/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.14 h1:55VbUWoBxE1iTAh3B6JztD6xyQ06CvW/31oD6rYwrtY=
|
||||
github.com/creack/pty v1.1.14/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
|
||||
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=
|
||||
@@ -531,8 +550,9 @@ github.com/gobuffalo/validate/v3 v3.1.0/go.mod h1:HFpjq+AIiA2RHoQnQVTFKF/ZpUPXwy
|
||||
github.com/gobuffalo/validate/v3 v3.2.0/go.mod h1:PrhDOdDHxtN8KUgMvF3TDL0r1YZXV4sQnyFX/EmeETY=
|
||||
github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc=
|
||||
github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY=
|
||||
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
|
||||
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI=
|
||||
@@ -556,15 +576,18 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||
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 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.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=
|
||||
@@ -572,8 +595,11 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
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.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -583,8 +609,13 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
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/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
|
||||
@@ -593,11 +624,19 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.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/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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -633,8 +672,9 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
|
||||
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.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -661,6 +701,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
@@ -716,8 +757,9 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@@ -766,8 +808,9 @@ github.com/luna-duclos/instrumentedsql v0.0.0-20181127104832-b7d587d28109/go.mod
|
||||
github.com/luna-duclos/instrumentedsql v1.1.2/go.mod h1:4LGbEqDnopzNAiyxPPDXhLspyunZxgPTMJBKtC6U0BQ=
|
||||
github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -798,6 +841,7 @@ github.com/markbates/safe v1.0.0/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kN
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc=
|
||||
github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w=
|
||||
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@@ -835,8 +879,9 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
|
||||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
|
||||
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
|
||||
@@ -936,8 +981,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
|
||||
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
|
||||
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
@@ -984,6 +1030,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@@ -1056,8 +1103,9 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.3.2 h1:GDarE4TJQI52kYSbSAmLiId1Elfj+xgSDqrUZxFhxlU=
|
||||
github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
@@ -1070,8 +1118,8 @@ github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN
|
||||
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
|
||||
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
@@ -1087,6 +1135,7 @@ github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=
|
||||
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693/go.mod h1:6hSY48PjDm4UObWmGLyJE9DxYVKTgR9kbCspXXJEhcU=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
@@ -1103,6 +1152,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tdewolff/minify/v2 v2.9.20 h1:Fut7w3T7nWfDOb/bOgyEvshQRRMt+xzi1T7spEEKXDw=
|
||||
github.com/tdewolff/minify/v2 v2.9.20/go.mod h1:PoDBts2L7sCwUT28vTAlozGeD6qxjrrihtin4bR/RMM=
|
||||
github.com/tdewolff/parse/v2 v2.5.19 h1:Kjaj3KQOx/4elIxlBSglus4E2oMfdROphvbq2b+OBZ0=
|
||||
github.com/tdewolff/parse/v2 v2.5.19/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
||||
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
|
||||
github.com/tidwall/gjson v1.7.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||
@@ -1135,8 +1190,11 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
|
||||
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=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0=
|
||||
@@ -1149,6 +1207,9 @@ go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo=
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||
go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
|
||||
@@ -1158,17 +1219,23 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
|
||||
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/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.13.0/go.mod h1:TwTkyRaTam1pOIb2wxcAiC2hkMVbokXkt6DEt5nDkD8=
|
||||
go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
|
||||
go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||
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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
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.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
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-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1237,8 +1304,9 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
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 h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
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=
|
||||
@@ -1247,8 +1315,11 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
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.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ=
|
||||
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/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 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1288,13 +1359,25 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
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-20200520004742-59133d7f0dd7/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-20200602114024-627f9648deb9/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-20210224082022-3d97a244fca7/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-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1302,8 +1385,15 @@ golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAG
|
||||
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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
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-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/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=
|
||||
@@ -1312,7 +1402,9 @@ golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1376,16 +1468,31 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/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-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
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-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1479,14 +1586,31 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK
|
||||
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-20200308013534-11ec41452d41/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-20200505023115-26f46d2f7ef8/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-20200522201501-cb1345f3a375/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 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
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 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1509,13 +1633,27 @@ google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb
|
||||
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.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
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=
|
||||
@@ -1535,11 +1673,33 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx
|
||||
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-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8=
|
||||
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-20201110150050-8816d57aaa9a/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-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
@@ -1551,8 +1711,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
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 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.27.0/go.mod h1:Sp1lku8WJMvNV0kjDI4Ni/T7J/U3BO5ct5kEaoVU8+I=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
@@ -1575,8 +1737,9 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
@@ -1592,6 +1755,7 @@ gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmC
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.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.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -1601,8 +1765,9 @@ 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-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
@@ -1612,18 +1777,19 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
|
||||
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=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
k8s.io/api v0.21.1 h1:94bbZ5NTjdINJEdzOkpS4vdPhkb1VFpTYC9zh43f75c=
|
||||
k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s=
|
||||
k8s.io/apimachinery v0.21.1 h1:Q6XuHGlj2xc+hlMCvqyYfbv3H7SRGn2c8NycxJquDVs=
|
||||
k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY=
|
||||
k8s.io/apiserver v0.21.1 h1:wTRcid53IhxhbFt4KTrFSw8tAncfr01EP91lzfcygVg=
|
||||
k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY=
|
||||
k8s.io/client-go v0.21.1 h1:bhblWYLZKUu+pm50plvQF8WpY6TXdRRtcS/K9WauOj4=
|
||||
k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs=
|
||||
k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q=
|
||||
k8s.io/component-base v0.21.1 h1:iLpj2btXbR326s/xNQWmPNGu0gaYSjzn7IN/5i28nQw=
|
||||
k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA=
|
||||
k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ=
|
||||
k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg=
|
||||
k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII=
|
||||
k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI=
|
||||
k8s.io/apiserver v0.21.3 h1:QxAgE1ZPQG5cPlHScHTnLxP9H/kU3zjH1Vnd8G+n5OI=
|
||||
k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU=
|
||||
k8s.io/client-go v0.21.3 h1:J9nxZTOmvkInRDCzcSNQmPJbDYN/PjlxXT9Mos3HcLg=
|
||||
k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU=
|
||||
k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo=
|
||||
k8s.io/component-base v0.21.3 h1:4WuuXY3Npa+iFfi2aDRiOz+anhNvRfye0859ZgfC5Og=
|
||||
k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ=
|
||||
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
|
||||
k8s.io/gengo v0.0.0-20210203185629-de9496dff47b h1:bAU8IlrMA6KbP0dIg/sVSJn95pDCUHDZx0DpTGrf2v4=
|
||||
@@ -1631,10 +1797,10 @@ k8s.io/gengo v0.0.0-20210203185629-de9496dff47b/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAE
|
||||
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
|
||||
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
|
||||
k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
|
||||
k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM=
|
||||
k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
|
||||
k8s.io/kube-aggregator v0.21.1 h1:3pPRhOXZcJYjNDjPDizFx0G5//DArWKANZE03J5z8Ck=
|
||||
k8s.io/kube-aggregator v0.21.1/go.mod h1:cAZ0n02IiSl57sQSHz4vvrz3upQRMbytOiZnpPJaQzQ=
|
||||
k8s.io/klog/v2 v2.10.0 h1:R2HDMDJsHVTHA2n4RjwbeYXdOcBymXdX/JRb1v0VGhE=
|
||||
k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
|
||||
k8s.io/kube-aggregator v0.21.3 h1:jS/6ZZGPCkBQhzGGusAd2St+KP/FtQBCXOCOo3H7/U4=
|
||||
k8s.io/kube-aggregator v0.21.3/go.mod h1:9OIUuR5KIsNZYP/Xsh4HBsaqbS7ICJpRz3XSKtKajRc=
|
||||
k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0=
|
||||
k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE=
|
||||
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
|
||||
@@ -1649,11 +1815,11 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
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/apiserver-network-proxy/konnectivity-client v0.0.15 h1:4uqm9Mv+w2MmBYD+F4qf/v6tDFUdPOk29C095RbU5mY=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19 h1:0jaDAAxtqIrrqas4vtTqxct4xS5kHfRNycTRLTyJmVM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
||||
@@ -90,7 +90,12 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
|
||||
err := c.k8sClient.
|
||||
CoreV1().
|
||||
Secrets(c.namespace).
|
||||
Delete(ctx.Context, c.certsSecretResourceName, metav1.DeleteOptions{})
|
||||
Delete(ctx.Context, c.certsSecretResourceName, metav1.DeleteOptions{
|
||||
Preconditions: &metav1.Preconditions{
|
||||
UID: &secret.UID,
|
||||
ResourceVersion: &secret.ResourceVersion,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// Do return an error here so that the controller library will reschedule
|
||||
// us to try deleting this cert again.
|
||||
|
||||
@@ -18,8 +18,11 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
@@ -223,14 +226,19 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
test.configKubeAPIClient(kubeAPIClient)
|
||||
}
|
||||
|
||||
testRV := "rv_001"
|
||||
testUID := types.UID("uid_002")
|
||||
|
||||
kubeInformerClient := kubernetesfake.NewSimpleClientset()
|
||||
name := certsSecretResourceName
|
||||
namespace := "some-namespace"
|
||||
if test.fillSecretData != nil {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
ResourceVersion: testRV,
|
||||
UID: testUID,
|
||||
},
|
||||
Data: map[string][]byte{},
|
||||
}
|
||||
@@ -245,10 +253,12 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
0,
|
||||
)
|
||||
|
||||
trackDeleteClient := &clientWrapper{Interface: kubeAPIClient, opts: &[]metav1.DeleteOptions{}}
|
||||
|
||||
c := NewCertsExpirerController(
|
||||
namespace,
|
||||
certsSecretResourceName,
|
||||
kubeAPIClient,
|
||||
trackDeleteClient,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
test.renewBefore,
|
||||
@@ -285,6 +295,46 @@ func TestExpirerControllerSync(t *testing.T) {
|
||||
}
|
||||
acActions := kubeAPIClient.Actions()
|
||||
require.Equal(t, exActions, acActions)
|
||||
|
||||
if test.wantDelete {
|
||||
require.Len(t, *trackDeleteClient.opts, 1)
|
||||
require.Equal(t, metav1.DeleteOptions{
|
||||
Preconditions: &metav1.Preconditions{
|
||||
UID: &testUID,
|
||||
ResourceVersion: &testRV,
|
||||
},
|
||||
}, (*trackDeleteClient.opts)[0])
|
||||
} else {
|
||||
require.Len(t, *trackDeleteClient.opts, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type clientWrapper struct {
|
||||
kubernetes.Interface
|
||||
opts *[]metav1.DeleteOptions
|
||||
}
|
||||
|
||||
func (c *clientWrapper) CoreV1() corev1client.CoreV1Interface {
|
||||
return &coreWrapper{CoreV1Interface: c.Interface.CoreV1(), opts: c.opts}
|
||||
}
|
||||
|
||||
type coreWrapper struct {
|
||||
corev1client.CoreV1Interface
|
||||
opts *[]metav1.DeleteOptions
|
||||
}
|
||||
|
||||
func (c *coreWrapper) Secrets(namespace string) corev1client.SecretInterface {
|
||||
return &secretsWrapper{SecretInterface: c.CoreV1Interface.Secrets(namespace), opts: c.opts}
|
||||
}
|
||||
|
||||
type secretsWrapper struct {
|
||||
corev1client.SecretInterface
|
||||
opts *[]metav1.DeleteOptions
|
||||
}
|
||||
|
||||
func (s *secretsWrapper) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
||||
*s.opts = append(*s.opts, opts)
|
||||
return s.SecretInterface.Delete(ctx, name, opts)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -53,6 +55,7 @@ const (
|
||||
caCrtKey = "ca.crt"
|
||||
caKeyKey = "ca.key"
|
||||
appLabelKey = "app"
|
||||
annotationKeysKey = "credentialissuer.pinniped.dev/annotation-keys"
|
||||
)
|
||||
|
||||
type impersonatorConfigController struct {
|
||||
@@ -140,7 +143,15 @@ func NewImpersonatorConfigController(
|
||||
withInformer(
|
||||
servicesInformer,
|
||||
pinnipedcontroller.SimpleFilterWithSingletonQueue(func(obj metav1.Object) bool {
|
||||
return obj.GetNamespace() == namespace && obj.GetName() == generatedLoadBalancerServiceName
|
||||
if obj.GetNamespace() != namespace {
|
||||
return false
|
||||
}
|
||||
switch obj.GetName() {
|
||||
case generatedLoadBalancerServiceName, generatedClusterIPServiceName:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
@@ -521,34 +532,93 @@ func (c *impersonatorConfigController) ensureClusterIPServiceIsStopped(ctx conte
|
||||
return utilerrors.FilterOut(err, k8serrors.IsNotFound)
|
||||
}
|
||||
|
||||
func (c *impersonatorConfigController) createOrUpdateService(ctx context.Context, service *v1.Service) error {
|
||||
log := c.infoLog.WithValues("serviceType", service.Spec.Type, "service", klog.KObj(service))
|
||||
existing, err := c.servicesInformer.Lister().Services(c.namespace).Get(service.Name)
|
||||
func (c *impersonatorConfigController) createOrUpdateService(ctx context.Context, desiredService *v1.Service) error {
|
||||
log := c.infoLog.WithValues("serviceType", desiredService.Spec.Type, "service", klog.KObj(desiredService))
|
||||
|
||||
// Prepare to remember which annotation keys were added from the CredentialIssuer spec, both for
|
||||
// creates and for updates, in case someone removes a key from the spec in the future. We would like
|
||||
// to be able to detect that the missing key means that we should remove the key. This is needed to
|
||||
// differentiate it from a key that was added by another actor, which we should not remove.
|
||||
// But don't bother recording the requested annotations if there were no annotations requested.
|
||||
desiredAnnotationKeys := make([]string, 0, len(desiredService.Annotations))
|
||||
for k := range desiredService.Annotations {
|
||||
desiredAnnotationKeys = append(desiredAnnotationKeys, k)
|
||||
}
|
||||
if len(desiredAnnotationKeys) > 0 {
|
||||
// Sort them since they come out of the map in no particular order.
|
||||
sort.Strings(desiredAnnotationKeys)
|
||||
keysJSONArray, err := json.Marshal(desiredAnnotationKeys)
|
||||
if err != nil {
|
||||
return err // This shouldn't really happen. We should always be able to marshal an array of strings.
|
||||
}
|
||||
// Save the desired annotations to a bookkeeping annotation.
|
||||
desiredService.Annotations[annotationKeysKey] = string(keysJSONArray)
|
||||
}
|
||||
|
||||
// Get the Service from the informer, and create it if it does not already exist.
|
||||
existingService, err := c.servicesInformer.Lister().Services(c.namespace).Get(desiredService.Name)
|
||||
if k8serrors.IsNotFound(err) {
|
||||
log.Info("creating service for impersonation proxy")
|
||||
_, err := c.k8sClient.CoreV1().Services(c.namespace).Create(ctx, service, metav1.CreateOptions{})
|
||||
_, err := c.k8sClient.CoreV1().Services(c.namespace).Create(ctx, desiredService, metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update only the specific fields that are meaningfully part of our desired state.
|
||||
updated := existing.DeepCopy()
|
||||
updated.ObjectMeta.Labels = service.ObjectMeta.Labels
|
||||
updated.ObjectMeta.Annotations = service.ObjectMeta.Annotations
|
||||
updated.Spec.LoadBalancerIP = service.Spec.LoadBalancerIP
|
||||
updated.Spec.Type = service.Spec.Type
|
||||
updated.Spec.Selector = service.Spec.Selector
|
||||
// The Service already exists, so update only the specific fields that are meaningfully part of our desired state.
|
||||
updatedService := existingService.DeepCopy()
|
||||
updatedService.ObjectMeta.Labels = desiredService.ObjectMeta.Labels
|
||||
updatedService.Spec.LoadBalancerIP = desiredService.Spec.LoadBalancerIP
|
||||
updatedService.Spec.Type = desiredService.Spec.Type
|
||||
updatedService.Spec.Selector = desiredService.Spec.Selector
|
||||
|
||||
// Do not simply overwrite the existing annotations with the desired annotations. Instead, merge-overwrite.
|
||||
// Another actor in the system, like a human user or a non-Pinniped controller, might have updated the
|
||||
// existing Service's annotations. If they did, then we do not want to overwrite those keys expect for
|
||||
// the specific keys that are from the CredentialIssuer's spec, because if we overwrite keys belonging
|
||||
// to another controller then we could end up infinitely flapping back and forth with the other controller,
|
||||
// both updating that annotation on the Service.
|
||||
if updatedService.Annotations == nil {
|
||||
updatedService.Annotations = map[string]string{}
|
||||
}
|
||||
for k, v := range desiredService.Annotations {
|
||||
updatedService.Annotations[k] = v
|
||||
}
|
||||
|
||||
// Check if the the existing Service contains a record of previous annotations that were added by this controller.
|
||||
// Note that in an upgrade, older versions of Pinniped might have created the Service without this bookkeeping annotation.
|
||||
oldDesiredAnnotationKeysJSON, foundOldDesiredAnnotationKeysJSON := existingService.Annotations[annotationKeysKey]
|
||||
oldDesiredAnnotationKeys := []string{}
|
||||
if foundOldDesiredAnnotationKeysJSON {
|
||||
_ = json.Unmarshal([]byte(oldDesiredAnnotationKeysJSON), &oldDesiredAnnotationKeys)
|
||||
// In the unlikely event that we cannot parse the value of our bookkeeping annotation, just act like it
|
||||
// wasn't present and update it to the new value that it should have based on the current desired state.
|
||||
}
|
||||
|
||||
// Check if any annotations which were previously in the CredentialIssuer spec are now gone from the spec,
|
||||
// which means that those now-missing annotations should get deleted.
|
||||
for _, oldKey := range oldDesiredAnnotationKeys {
|
||||
if _, existsInDesired := desiredService.Annotations[oldKey]; !existsInDesired {
|
||||
delete(updatedService.Annotations, oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
// If no annotations were requested, then remove the special bookkeeping annotation which might be
|
||||
// leftover from a previous update. During the next update, non-existence will be taken to mean
|
||||
// that no annotations were previously requested by the CredentialIssuer spec.
|
||||
if len(desiredAnnotationKeys) == 0 {
|
||||
delete(updatedService.Annotations, annotationKeysKey)
|
||||
}
|
||||
|
||||
// If our updates didn't change anything, we're done.
|
||||
if equality.Semantic.DeepEqual(existing, updated) {
|
||||
if equality.Semantic.DeepEqual(existingService, updatedService) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise apply the updates.
|
||||
c.infoLog.Info("updating service for impersonation proxy")
|
||||
_, err = c.k8sClient.CoreV1().Services(c.namespace).Update(ctx, updated, metav1.UpdateOptions{})
|
||||
_, err = c.k8sClient.CoreV1().Services(c.namespace).Update(ctx, updatedService, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -131,11 +131,12 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
|
||||
|
||||
when("watching Service objects", func() {
|
||||
var subject controllerlib.Filter
|
||||
var target, wrongNamespace, wrongName, unrelated *corev1.Service
|
||||
var targetLBService, targetClusterIPService, wrongNamespace, wrongName, unrelated *corev1.Service
|
||||
|
||||
it.Before(func() {
|
||||
subject = servicesInformerFilter
|
||||
target = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: generatedLoadBalancerServiceName, Namespace: installedInNamespace}}
|
||||
targetLBService = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: generatedLoadBalancerServiceName, Namespace: installedInNamespace}}
|
||||
targetClusterIPService = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: generatedClusterIPServiceName, Namespace: installedInNamespace}}
|
||||
wrongNamespace = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: generatedLoadBalancerServiceName, Namespace: "wrong-namespace"}}
|
||||
wrongName = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: installedInNamespace}}
|
||||
unrelated = &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}}
|
||||
@@ -143,10 +144,14 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
|
||||
|
||||
when("the target Service changes", func() {
|
||||
it("returns true to trigger the sync method", func() {
|
||||
r.True(subject.Add(target))
|
||||
r.True(subject.Update(target, unrelated))
|
||||
r.True(subject.Update(unrelated, target))
|
||||
r.True(subject.Delete(target))
|
||||
r.True(subject.Add(targetLBService))
|
||||
r.True(subject.Update(targetLBService, unrelated))
|
||||
r.True(subject.Update(unrelated, targetLBService))
|
||||
r.True(subject.Delete(targetLBService))
|
||||
r.True(subject.Add(targetClusterIPService))
|
||||
r.True(subject.Update(targetClusterIPService, unrelated))
|
||||
r.True(subject.Update(unrelated, targetClusterIPService))
|
||||
r.True(subject.Delete(targetClusterIPService))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -785,6 +790,13 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var addServiceToTrackers = func(service *corev1.Service, clients ...*kubernetesfake.Clientset) {
|
||||
for _, client := range clients {
|
||||
serviceCopy := service.DeepCopy()
|
||||
r.NoError(client.Tracker().Add(serviceCopy))
|
||||
}
|
||||
}
|
||||
|
||||
var deleteServiceFromTracker = func(resourceName string, client *kubernetesfake.Clientset) {
|
||||
r.NoError(client.Tracker().Delete(
|
||||
schema.GroupVersionResource{Version: "v1", Resource: "services"},
|
||||
@@ -1644,7 +1656,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
})
|
||||
|
||||
when("credentialissuer has service type loadbalancer and custom annotations", func() {
|
||||
annotations := map[string]string{"some-annotation-key": "some-annotation-value"}
|
||||
it.Before(func() {
|
||||
addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName},
|
||||
@@ -1653,7 +1664,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer,
|
||||
Annotations: annotations,
|
||||
Annotations: map[string]string{"some-annotation-key": "some-annotation-value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1667,7 +1678,10 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
r.Len(kubeAPIClient.Actions(), 3)
|
||||
requireNodesListed(kubeAPIClient.Actions()[0])
|
||||
lbService := requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1])
|
||||
require.Equal(t, lbService.Annotations, annotations)
|
||||
require.Equal(t, lbService.Annotations, map[string]string{
|
||||
"some-annotation-key": "some-annotation-value",
|
||||
"credentialissuer.pinniped.dev/annotation-keys": `["some-annotation-key"]`,
|
||||
})
|
||||
requireCASecretWasCreated(kubeAPIClient.Actions()[2])
|
||||
requireTLSServerIsRunningWithoutCerts()
|
||||
requireCredentialIssuer(newPendingStrategyWaitingForLB())
|
||||
@@ -2386,20 +2400,30 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
|
||||
// Simulate another actor in the system, like a human user or a non-Pinniped controller,
|
||||
// updating the new Service's annotations. The map was nil, so we can overwrite the whole thing,
|
||||
lbService.Annotations = map[string]string{
|
||||
"annotation-from-unrelated-controller-key": "annotation-from-unrelated-controller-val",
|
||||
"my-annotation-key": "my-annotation-from-unrelated-controller-val",
|
||||
}
|
||||
|
||||
// Simulate the informer cache's background update from its watch.
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services())
|
||||
addObjectToKubeInformerAndWait(lbService, kubeInformers.Core().V1().Services())
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets())
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Secrets())
|
||||
|
||||
// Add annotations to the spec.
|
||||
annotations := map[string]string{"my-annotation-key": "my-annotation-val"}
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 4) // no new actions because the controller decides there is nothing to update on the Service
|
||||
|
||||
// Add annotations to the CredentialIssuer spec.
|
||||
credentialIssuerAnnotations := map[string]string{"my-annotation-key": "my-annotation-val"}
|
||||
updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{
|
||||
ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
ExternalEndpoint: localhostIP,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer,
|
||||
Annotations: annotations,
|
||||
Annotations: credentialIssuerAnnotations,
|
||||
},
|
||||
},
|
||||
}, pinnipedInformers.Config().V1alpha1().CredentialIssuers())
|
||||
@@ -2407,7 +2431,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 5) // one more item to update the loadbalancer
|
||||
lbService = requireLoadBalancerWasUpdated(kubeAPIClient.Actions()[4])
|
||||
require.Equal(t, annotations, lbService.Annotations) // now the annotations should exist on the load balancer
|
||||
require.Equal(t, map[string]string{
|
||||
// Now the CredentialIssuer annotations should be merged on the load balancer.
|
||||
// In the unlikely case where keys conflict, the CredentialIssuer value overwrites the other value.
|
||||
// Otherwise the annotations from the other actor should not be modified.
|
||||
"annotation-from-unrelated-controller-key": "annotation-from-unrelated-controller-val",
|
||||
"my-annotation-key": "my-annotation-val",
|
||||
"credentialissuer.pinniped.dev/annotation-keys": `["my-annotation-key"]`,
|
||||
}, lbService.Annotations)
|
||||
requireTLSServerIsRunning(ca, testServerAddr(), nil)
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
@@ -2447,20 +2478,30 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
|
||||
// Simulate another actor in the system, like a human user or a non-Pinniped controller,
|
||||
// updating the new Service's annotations.
|
||||
clusterIPService.Annotations = map[string]string{
|
||||
"annotation-from-unrelated-controller-key": "annotation-from-unrelated-controller-val",
|
||||
"my-annotation-key": "my-annotation-from-unrelated-controller-val",
|
||||
}
|
||||
|
||||
// Simulate the informer cache's background update from its watch.
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services())
|
||||
addObjectToKubeInformerAndWait(clusterIPService, kubeInformers.Core().V1().Services())
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets())
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Secrets())
|
||||
|
||||
// Add annotations to the spec.
|
||||
annotations := map[string]string{"my-annotation-key": "my-annotation-val"}
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 4) // no new actions because the controller decides there is nothing to update on the Service
|
||||
|
||||
// Add annotations to the CredentialIssuer spec.
|
||||
credentialIssuerAnnotations := map[string]string{"my-annotation-key": "my-annotation-val"}
|
||||
updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{
|
||||
ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
ExternalEndpoint: localhostIP,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP,
|
||||
Annotations: annotations,
|
||||
Annotations: credentialIssuerAnnotations,
|
||||
},
|
||||
},
|
||||
}, pinnipedInformers.Config().V1alpha1().CredentialIssuers())
|
||||
@@ -2468,7 +2509,173 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 5) // one more item to update the loadbalancer
|
||||
clusterIPService = requireClusterIPWasUpdated(kubeAPIClient.Actions()[4])
|
||||
require.Equal(t, annotations, clusterIPService.Annotations) // now the annotations should exist on the load balancer
|
||||
require.Equal(t, map[string]string{
|
||||
// Now the CredentialIssuer annotations should be merged on the load balancer.
|
||||
// In the unlikely case where keys conflict, the CredentialIssuer value overwrites the other value.
|
||||
// Otherwise the annotations from the other actor should not be modified.
|
||||
"annotation-from-unrelated-controller-key": "annotation-from-unrelated-controller-val",
|
||||
"my-annotation-key": "my-annotation-val",
|
||||
"credentialissuer.pinniped.dev/annotation-keys": `["my-annotation-key"]`,
|
||||
}, clusterIPService.Annotations)
|
||||
requireTLSServerIsRunning(ca, testServerAddr(), nil)
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
})
|
||||
})
|
||||
|
||||
when("requesting a load balancer via CredentialIssuer with annotations, then updating the CredentialIssuer annotations to remove one", func() {
|
||||
it.Before(func() {
|
||||
addSecretToTrackers(signingCASecret, kubeInformerClient)
|
||||
addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName},
|
||||
Spec: v1alpha1.CredentialIssuerSpec{
|
||||
ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
ExternalEndpoint: localhostIP,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer,
|
||||
Annotations: map[string]string{
|
||||
"my-initial-annotation1-key": "my-initial-annotation1-val",
|
||||
"my-initial-annotation2-key": "my-initial-annotation2-val",
|
||||
"my-initial-annotation3-key": "my-initial-annotation3-val",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pinnipedInformerClient, pinnipedAPIClient)
|
||||
addNodeWithRoleToTracker("worker", kubeAPIClient)
|
||||
})
|
||||
|
||||
it("creates the load balancer with annotations, then removes the removed annotation", func() {
|
||||
startInformersAndController()
|
||||
|
||||
// Should have started in "enabled" mode with service type load balancer, so one is created.
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 4)
|
||||
requireNodesListed(kubeAPIClient.Actions()[0])
|
||||
lbService := requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1])
|
||||
require.Equal(t, map[string]string{
|
||||
"my-initial-annotation1-key": "my-initial-annotation1-val",
|
||||
"my-initial-annotation2-key": "my-initial-annotation2-val",
|
||||
"my-initial-annotation3-key": "my-initial-annotation3-val",
|
||||
"credentialissuer.pinniped.dev/annotation-keys": `["my-initial-annotation1-key","my-initial-annotation2-key","my-initial-annotation3-key"]`,
|
||||
}, lbService.Annotations) // there should be some annotations at first
|
||||
ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2])
|
||||
requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca)
|
||||
requireTLSServerIsRunning(ca, testServerAddr(), nil)
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
|
||||
// Simulate another actor in the system, like a human user or a non-Pinniped controller,
|
||||
// updating the new Service to add another annotation.
|
||||
lbService.Annotations["annotation-from-unrelated-controller-key"] = "annotation-from-unrelated-controller-val"
|
||||
|
||||
// Simulate the informer cache's background update from its watch.
|
||||
addObjectToKubeInformerAndWait(lbService, kubeInformers.Core().V1().Services())
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets())
|
||||
addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Secrets())
|
||||
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 4) // no new actions because the controller decides there is nothing to update on the Service
|
||||
|
||||
// Remove one of the annotations from the CredentialIssuer spec.
|
||||
updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{
|
||||
ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
ExternalEndpoint: localhostIP,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer,
|
||||
Annotations: map[string]string{
|
||||
"my-initial-annotation1-key": "my-initial-annotation1-val",
|
||||
"my-initial-annotation3-key": "my-initial-annotation3-val",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pinnipedInformers.Config().V1alpha1().CredentialIssuers())
|
||||
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 5) // one more item to update the loadbalancer
|
||||
lbService = requireLoadBalancerWasUpdated(kubeAPIClient.Actions()[4])
|
||||
require.Equal(t, map[string]string{
|
||||
// Now the CredentialIssuer annotations should be merged on the load balancer.
|
||||
// Since the user removed the "my-initial-annotation2-key" key from the CredentialIssuer spec,
|
||||
// it should be removed from the Service.
|
||||
// The annotations from the other actor should not be modified.
|
||||
"annotation-from-unrelated-controller-key": "annotation-from-unrelated-controller-val",
|
||||
"my-initial-annotation1-key": "my-initial-annotation1-val",
|
||||
"my-initial-annotation3-key": "my-initial-annotation3-val",
|
||||
"credentialissuer.pinniped.dev/annotation-keys": `["my-initial-annotation1-key","my-initial-annotation3-key"]`,
|
||||
}, lbService.Annotations)
|
||||
requireTLSServerIsRunning(ca, testServerAddr(), nil)
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
|
||||
// Remove all the rest of the annotations from the CredentialIssuer spec so there are none remaining.
|
||||
updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{
|
||||
ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
ExternalEndpoint: localhostIP,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer,
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
}, pinnipedInformers.Config().V1alpha1().CredentialIssuers())
|
||||
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 6) // one more item to update the loadbalancer
|
||||
lbService = requireLoadBalancerWasUpdated(kubeAPIClient.Actions()[5])
|
||||
require.Equal(t, map[string]string{
|
||||
// Since the user removed all annotations from the CredentialIssuer spec,
|
||||
// they should all be removed from the Service, along with the special bookkeeping annotation too.
|
||||
// The annotations from the other actor should not be modified.
|
||||
"annotation-from-unrelated-controller-key": "annotation-from-unrelated-controller-val",
|
||||
}, lbService.Annotations)
|
||||
requireTLSServerIsRunning(ca, testServerAddr(), nil)
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
})
|
||||
})
|
||||
|
||||
when("requesting a load balancer via CredentialIssuer, but there is already a load balancer with an invalid bookkeeping annotation value", func() {
|
||||
it.Before(func() {
|
||||
addSecretToTrackers(signingCASecret, kubeInformerClient)
|
||||
addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName},
|
||||
Spec: v1alpha1.CredentialIssuerSpec{
|
||||
ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{
|
||||
Mode: v1alpha1.ImpersonationProxyModeEnabled,
|
||||
ExternalEndpoint: localhostIP,
|
||||
Service: v1alpha1.ImpersonationProxyServiceSpec{
|
||||
Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer,
|
||||
Annotations: map[string]string{"some-annotation": "annotation-value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pinnipedInformerClient, pinnipedAPIClient)
|
||||
addNodeWithRoleToTracker("worker", kubeAPIClient)
|
||||
// Add a Service with a messed up bookkeeping annotation.
|
||||
loadBalancerService := newLoadBalancerService(loadBalancerServiceName, corev1.ServiceStatus{})
|
||||
loadBalancerService.Annotations = map[string]string{
|
||||
annotationKeysKey: `["this is not valid json`,
|
||||
}
|
||||
addServiceToTrackers(loadBalancerService, kubeInformerClient, kubeAPIClient)
|
||||
})
|
||||
|
||||
it("just acts like the annotation wasn't present since that is better than becoming inoperable", func() {
|
||||
startInformersAndController()
|
||||
|
||||
// Should have started in "enabled" mode with service type load balancer, so one is created.
|
||||
r.NoError(runControllerSync())
|
||||
r.Len(kubeAPIClient.Actions(), 4)
|
||||
requireNodesListed(kubeAPIClient.Actions()[0])
|
||||
lbService := requireLoadBalancerWasUpdated(kubeAPIClient.Actions()[1])
|
||||
require.Equal(t, map[string]string{
|
||||
"some-annotation": "annotation-value",
|
||||
"credentialissuer.pinniped.dev/annotation-keys": `["some-annotation"]`,
|
||||
}, lbService.Annotations)
|
||||
ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2])
|
||||
requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca)
|
||||
requireTLSServerIsRunning(ca, testServerAddr(), nil)
|
||||
requireCredentialIssuer(newSuccessStrategy(localhostIP, ca))
|
||||
requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM)
|
||||
|
||||
@@ -207,16 +207,17 @@ func TestAgentController(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
discoveryURLOverride *string
|
||||
pinnipedObjects []runtime.Object
|
||||
kubeObjects []runtime.Object
|
||||
addKubeReactions func(*kubefake.Clientset)
|
||||
mocks func(*testing.T, *mocks.MockPodCommandExecutorMockRecorder, *mocks.MockDynamicCertPrivateMockRecorder, *cache.Expiring)
|
||||
wantDistinctErrors []string
|
||||
wantDistinctLogs []string
|
||||
wantAgentDeployment *appsv1.Deployment
|
||||
wantStrategy *configv1alpha1.CredentialIssuerStrategy
|
||||
name string
|
||||
discoveryURLOverride *string
|
||||
pinnipedObjects []runtime.Object
|
||||
kubeObjects []runtime.Object
|
||||
addKubeReactions func(*kubefake.Clientset)
|
||||
mocks func(*testing.T, *mocks.MockPodCommandExecutorMockRecorder, *mocks.MockDynamicCertPrivateMockRecorder, *cache.Expiring)
|
||||
wantDistinctErrors []string
|
||||
alsoAllowUndesiredDistinctErrors []string
|
||||
wantDistinctLogs []string
|
||||
wantAgentDeployment *appsv1.Deployment
|
||||
wantStrategy *configv1alpha1.CredentialIssuerStrategy
|
||||
}{
|
||||
{
|
||||
name: "no CredentialIssuer found",
|
||||
@@ -351,6 +352,10 @@ func TestAgentController(t *testing.T) {
|
||||
wantDistinctErrors: []string{
|
||||
"could not find a healthy agent pod (1 candidate)",
|
||||
},
|
||||
alsoAllowUndesiredDistinctErrors: []string{
|
||||
// due to the high amount of nondeterminism in this test, this error will sometimes also happen, but is not required to happen
|
||||
`could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`,
|
||||
},
|
||||
wantDistinctLogs: []string{
|
||||
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`,
|
||||
},
|
||||
@@ -395,6 +400,10 @@ func TestAgentController(t *testing.T) {
|
||||
wantDistinctErrors: []string{
|
||||
"could not find a healthy agent pod (1 candidate)",
|
||||
},
|
||||
alsoAllowUndesiredDistinctErrors: []string{
|
||||
// due to the high amount of nondeterminism in this test, this error will sometimes also happen, but is not required to happen
|
||||
`could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`,
|
||||
},
|
||||
wantDistinctLogs: []string{
|
||||
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`,
|
||||
},
|
||||
@@ -756,7 +765,14 @@ func TestAgentController(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
errorMessages := runControllerUntilQuiet(ctx, t, controller, kubeInformers, conciergeInformers)
|
||||
assert.Equal(t, tt.wantDistinctErrors, deduplicate(errorMessages), "unexpected errors")
|
||||
|
||||
actualErrors := deduplicate(errorMessages)
|
||||
require.Subsetf(t, actualErrors, tt.wantDistinctErrors, "required error(s) were not found in the actual errors")
|
||||
|
||||
allAllowedErrors := append([]string{}, tt.wantDistinctErrors...)
|
||||
allAllowedErrors = append(allAllowedErrors, tt.alsoAllowUndesiredDistinctErrors...)
|
||||
require.Subsetf(t, allAllowedErrors, actualErrors, "actual errors contained additional error(s) which is not expected by the test")
|
||||
|
||||
assert.Equal(t, tt.wantDistinctLogs, deduplicate(log.Lines()), "unexpected logs")
|
||||
|
||||
// Assert that the agent deployment is in the expected final state.
|
||||
|
||||
@@ -263,7 +263,14 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
httpClient = &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
|
||||
httpClient = &http.Client{
|
||||
Timeout: time.Minute,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
|
||||
discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -797,6 +798,17 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
|
||||
require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim())
|
||||
require.Equal(t, tt.wantResultingCache[i].GetGroupsClaim(), actualIDP.GetGroupsClaim())
|
||||
require.ElementsMatch(t, tt.wantResultingCache[i].GetScopes(), actualIDP.GetScopes())
|
||||
|
||||
// We always want to use the proxy from env on these clients, so although the following assertions
|
||||
// are a little hacky, this is a cheap way to test that we are using it.
|
||||
actualTransport, ok := actualIDP.Client.Transport.(*http.Transport)
|
||||
require.True(t, ok, "expected cached provider to have client with Transport of type *http.Transport")
|
||||
httpProxyFromEnvFunction := reflect.ValueOf(http.ProxyFromEnvironment).Pointer()
|
||||
actualTransportProxyFunction := reflect.ValueOf(actualTransport.Proxy).Pointer()
|
||||
require.Equal(t, httpProxyFromEnvFunction, actualTransportProxyFunction,
|
||||
"Transport should have used http.ProxyFromEnvironment as its Proxy func")
|
||||
// We also want a reasonable timeout on each request/response cycle for OIDC discovery and JWKS.
|
||||
require.Equal(t, time.Minute, actualIDP.Client.Timeout)
|
||||
}
|
||||
|
||||
actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().OIDCIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{})
|
||||
|
||||
@@ -118,7 +118,6 @@ func (c *tlsCertObserverController) certFromSecret(ns string, secretName string)
|
||||
}
|
||||
|
||||
func lowercaseHostWithoutPort(issuerURL *url.URL) string {
|
||||
lowercaseHost := strings.ToLower(issuerURL.Host)
|
||||
colonSegments := strings.Split(lowercaseHost, ":")
|
||||
return colonSegments[0]
|
||||
lowercaseHost := strings.ToLower(issuerURL.Hostname())
|
||||
return lowercaseHost
|
||||
}
|
||||
|
||||
@@ -279,6 +279,17 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "good-tls-secret-name2"},
|
||||
},
|
||||
}
|
||||
federationDomainWithIPv6Issuer := &v1alpha1.FederationDomain{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ipv6-issuer-federationdomain",
|
||||
Namespace: installedInNamespace,
|
||||
},
|
||||
// Issuer hostname should be treated correctly when it is an IPv6 address. Test with a port number.
|
||||
Spec: v1alpha1.FederationDomainSpec{
|
||||
Issuer: "https://[2001:db8::1]:1234/path",
|
||||
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "good-tls-secret-name1"},
|
||||
},
|
||||
}
|
||||
testCrt1 := readTestFile("testdata/test.crt")
|
||||
r.NotEmpty(testCrt1)
|
||||
testCrt2 := readTestFile("testdata/test2.crt")
|
||||
@@ -309,6 +320,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithBadIssuer))
|
||||
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithGoodSecret1))
|
||||
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithGoodSecret2))
|
||||
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithIPv6Issuer))
|
||||
r.NoError(kubeInformerClient.Tracker().Add(goodTLSSecret1))
|
||||
r.NoError(kubeInformerClient.Tracker().Add(goodTLSSecret2))
|
||||
r.NoError(kubeInformerClient.Tracker().Add(badTLSSecret))
|
||||
@@ -322,7 +334,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
r.Nil(issuerTLSCertSetter.setDefaultTLSCertReceived)
|
||||
|
||||
r.True(issuerTLSCertSetter.setIssuerHostToTLSCertMapWasCalled)
|
||||
r.Len(issuerTLSCertSetter.issuerHostToTLSCertMapReceived, 2)
|
||||
r.Len(issuerTLSCertSetter.issuerHostToTLSCertMapReceived, 3)
|
||||
|
||||
// They keys in the map should be lower case and should not include the port numbers, because
|
||||
// TLS SNI says that SNI hostnames must be DNS names (not ports) and must be case insensitive.
|
||||
@@ -334,6 +346,10 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
actualCertificate2 := issuerTLSCertSetter.issuerHostToTLSCertMapReceived["www.issuer-with-good-secret2.com"]
|
||||
r.NotNil(actualCertificate2)
|
||||
r.Equal(expectedCertificate2, *actualCertificate2)
|
||||
|
||||
actualCertificate3 := issuerTLSCertSetter.issuerHostToTLSCertMapReceived["2001:db8::1"]
|
||||
r.NotNil(actualCertificate3)
|
||||
r.Equal(expectedCertificate1, *actualCertificate3)
|
||||
})
|
||||
|
||||
when("there is also a default TLS cert secret with the configured default TLS cert secret name", func() {
|
||||
@@ -366,7 +382,7 @@ func TestTLSCertObserverControllerSync(t *testing.T) {
|
||||
r.Equal(expectedDefaultCertificate, *actualDefaultCertificate)
|
||||
|
||||
r.True(issuerTLSCertSetter.setIssuerHostToTLSCertMapWasCalled)
|
||||
r.Len(issuerTLSCertSetter.issuerHostToTLSCertMapReceived, 2)
|
||||
r.Len(issuerTLSCertSetter.issuerHostToTLSCertMapReceived, 3)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
|
||||
"go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api"
|
||||
examplestart "go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/starter"
|
||||
"go.pinniped.dev/test/library"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
func TestExampleController(t *testing.T) {
|
||||
library.SkipUnlessIntegration(t)
|
||||
testlib.SkipUnlessIntegration(t)
|
||||
|
||||
config := library.NewClientConfig(t)
|
||||
config := testlib.NewClientConfig(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -93,7 +93,7 @@ done:
|
||||
expectedData := map[string][]byte{
|
||||
api.SecretDataKey: []byte(secretData),
|
||||
}
|
||||
require.Equal(t, expectedData, secret.Data, "expected to see new secret data: %s", library.Sdump(secret))
|
||||
require.Equal(t, expectedData, secret.Data, "expected to see new secret data: %s", testlib.Sdump(secret))
|
||||
break done // immediately stop consuming events because we want to check for updated events below
|
||||
|
||||
case <-timeout:
|
||||
@@ -132,7 +132,7 @@ done2:
|
||||
expectedData := map[string][]byte{
|
||||
api.SecretDataKey: []byte(secretData2),
|
||||
}
|
||||
require.Equal(t, expectedData, secret.Data, "expected to see updated secret data: %s", library.Sdump(secret))
|
||||
require.Equal(t, expectedData, secret.Data, "expected to see updated secret data: %s", testlib.Sdump(secret))
|
||||
break done2 // immediately stop consuming events because we want to check for hot loops below
|
||||
|
||||
case <-timeout:
|
||||
@@ -154,7 +154,7 @@ done3:
|
||||
}
|
||||
|
||||
// this assumes that no other actor in the system is trying to mutate this secret
|
||||
t.Errorf("unexpected event seen for secret: %s", library.Sdump(event))
|
||||
t.Errorf("unexpected event seen for secret: %s", testlib.Sdump(event))
|
||||
|
||||
case <-timeout:
|
||||
break done3 // we saw no events matching our secret meaning that we are not hot looping
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/test/library"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
func TestProviderWithDynamicServingCertificateController(t *testing.T) {
|
||||
@@ -205,12 +205,12 @@ func TestProviderWithDynamicServingCertificateController(t *testing.T) {
|
||||
if err != nil && lastTLSConfig != nil {
|
||||
// for debugging failures
|
||||
t.Log("diff between client CAs:\n", cmp.Diff(
|
||||
library.Sdump(wantClientCASubjects),
|
||||
library.Sdump(poolSubjects(lastTLSConfig.ClientCAs)),
|
||||
testlib.Sdump(wantClientCASubjects),
|
||||
testlib.Sdump(poolSubjects(lastTLSConfig.ClientCAs)),
|
||||
))
|
||||
t.Log("diff between serving certs:\n", cmp.Diff(
|
||||
library.Sdump(wantCerts),
|
||||
library.Sdump(lastTLSConfig.Certificates),
|
||||
testlib.Sdump(wantCerts),
|
||||
testlib.Sdump(lastTLSConfig.Certificates),
|
||||
))
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accesstoken
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,7 +109,7 @@ func (a *accessTokenStorage) getSession(ctx context.Context, signature string) (
|
||||
func newValidEmptyAccessTokenSession() *session {
|
||||
return &session{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package accesstoken
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
@@ -63,24 +65,25 @@ func TestAccessTokenStorage(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
}},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
@@ -138,13 +141,15 @@ func TestAccessTokenStorageRevocation(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Public: true,
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
TokenEndpointAuthMethod: "something",
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
TokenEndpointAuthMethod: "something",
|
||||
},
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
Session: &openid.DefaultSession{
|
||||
@@ -238,7 +243,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
@@ -248,7 +253,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
require.EqualError(t, err, "requester's client must be of type clientregistry.Client")
|
||||
}
|
||||
|
||||
func TestCreateWithoutRequesterID(t *testing.T) {
|
||||
@@ -257,7 +262,7 @@ func TestCreateWithoutRequesterID(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "", // empty ID
|
||||
Session: &openid.DefaultSession{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreateAccessTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authorizationcode
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -137,7 +138,7 @@ func (a *authorizeCodeStorage) getSession(ctx context.Context, signature string)
|
||||
func NewValidEmptyAuthorizeCodeSession() *AuthorizeCodeSession {
|
||||
return &AuthorizeCodeSession{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package authorizationcode
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
@@ -92,23 +93,25 @@ func TestAuthorizationCodeStorage(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
@@ -169,7 +172,7 @@ func TestInvalidateWhenConflictOnUpdateHappens(t *testing.T) {
|
||||
|
||||
request := &fosite.Request{
|
||||
ID: "some-request-id",
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
Session: &openid.DefaultSession{},
|
||||
}
|
||||
err := storage.CreateAuthorizeCodeSession(ctx, "fancy-signature", request)
|
||||
@@ -240,7 +243,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
@@ -250,7 +253,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreateAuthorizeCodeSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
require.EqualError(t, err, "requester's client must be of type clientregistry.Client")
|
||||
}
|
||||
|
||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, oauth2.AuthorizeCodeStorage) {
|
||||
@@ -270,7 +273,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
|
||||
require.Equal(t, validSession.Request, extractedRequest)
|
||||
|
||||
// checked above
|
||||
defaultClient := validSession.Request.Client.(*fosite.DefaultOpenIDConnectClient)
|
||||
defaultClient := validSession.Request.Client.(*clientregistry.Client)
|
||||
defaultSession := validSession.Request.Session.(*openid.DefaultSession)
|
||||
|
||||
// makes it easier to use a raw string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package fositestorage
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrInvalidRequestType = constable.Error("requester must be of type fosite.Request")
|
||||
ErrInvalidClientType = constable.Error("requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
ErrInvalidClientType = constable.Error("requester's client must be of type clientregistry.Client")
|
||||
ErrInvalidSessionType = constable.Error("requester's session must be of type openid.DefaultSession")
|
||||
StorageRequestIDLabelName = "storage.pinniped.dev/request-id" //nolint:gosec // this is not a credential
|
||||
)
|
||||
@@ -22,7 +23,7 @@ func ValidateAndExtractAuthorizeRequest(requester fosite.Requester) (*fosite.Req
|
||||
if !ok1 {
|
||||
return nil, ErrInvalidRequestType
|
||||
}
|
||||
_, ok2 := request.Client.(*fosite.DefaultOpenIDConnectClient)
|
||||
_, ok2 := request.Client.(*clientregistry.Client)
|
||||
if !ok2 {
|
||||
return nil, ErrInvalidClientType
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package openidconnect
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -110,7 +111,7 @@ func (a *openIDConnectRequestStorage) getSession(ctx context.Context, signature
|
||||
func newValidEmptyOIDCSession() *session {
|
||||
return &session{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package openidconnect
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
@@ -62,23 +64,25 @@ func TestOpenIdConnectStorage(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
@@ -176,7 +180,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
@@ -186,7 +190,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreateOpenIDConnectSession(ctx, "authcode.signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
require.EqualError(t, err, "requester's client must be of type clientregistry.Client")
|
||||
}
|
||||
|
||||
func TestAuthcodeHasNoDot(t *testing.T) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pkce
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -94,7 +95,7 @@ func (a *pkceStorage) getSession(ctx context.Context, signature string) (*sessio
|
||||
func newValidEmptyPKCESession() *session {
|
||||
return &session{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pkce
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
@@ -63,23 +65,25 @@ func TestPKCEStorage(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
@@ -183,7 +187,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
@@ -193,7 +197,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreatePKCERequestSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
require.EqualError(t, err, "requester's client must be of type clientregistry.Client")
|
||||
}
|
||||
|
||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, pkce.PKCERequestStorage) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package refreshtoken
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,7 +109,7 @@ func (a *refreshTokenStorage) getSession(ctx context.Context, signature string)
|
||||
func newValidEmptyRefreshTokenSession() *session {
|
||||
return &session{
|
||||
Request: &fosite.Request{
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
Session: &openid.DefaultSession{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package refreshtoken
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const namespace = "test-ns"
|
||||
@@ -62,23 +64,25 @@ func TestRefreshTokenStorage(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Secret: nil,
|
||||
RedirectURIs: nil,
|
||||
GrantTypes: nil,
|
||||
ResponseTypes: nil,
|
||||
Scopes: nil,
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
JSONWebKeys: nil,
|
||||
TokenEndpointAuthMethod: "something",
|
||||
RequestURIs: nil,
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: "",
|
||||
},
|
||||
RequestedScope: nil,
|
||||
GrantedScope: nil,
|
||||
@@ -137,13 +141,15 @@ func TestRefreshTokenStorageRevocation(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "abcd-1",
|
||||
RequestedAt: time.Time{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Public: true,
|
||||
Client: &clientregistry.Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinny",
|
||||
Public: true,
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
TokenEndpointAuthMethod: "something",
|
||||
},
|
||||
JSONWebKeysURI: "where",
|
||||
TokenEndpointAuthMethod: "something",
|
||||
},
|
||||
Form: url.Values{"key": []string{"val"}},
|
||||
Session: &openid.DefaultSession{
|
||||
@@ -237,7 +243,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
|
||||
request := &fosite.Request{
|
||||
Session: nil,
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's session must be of type openid.DefaultSession")
|
||||
@@ -247,7 +253,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
||||
Client: nil,
|
||||
}
|
||||
err = storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.EqualError(t, err, "requester's client must be of type fosite.DefaultOpenIDConnectClient")
|
||||
require.EqualError(t, err, "requester's client must be of type clientregistry.Client")
|
||||
}
|
||||
|
||||
func TestCreateWithoutRequesterID(t *testing.T) {
|
||||
@@ -256,7 +262,7 @@ func TestCreateWithoutRequesterID(t *testing.T) {
|
||||
request := &fosite.Request{
|
||||
ID: "", // empty ID
|
||||
Session: &openid.DefaultSession{},
|
||||
Client: &fosite.DefaultOpenIDConnectClient{},
|
||||
Client: &clientregistry.Client{},
|
||||
}
|
||||
err := storage.CreateRefreshTokenSession(ctx, "signature-doesnt-matter", request)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package securityheader implements an HTTP middleware for setting security-related response headers.
|
||||
package securityheader
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Wrap the provided http.Handler so it sets appropriate security-related response headers.
|
||||
func Wrap(wrapped http.Handler) http.Handler {
|
||||
return WrapWithCustomCSP(wrapped, "default-src 'none'; frame-ancestors 'none'")
|
||||
}
|
||||
|
||||
func WrapWithCustomCSP(wrapped http.Handler, cspHeader string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
|
||||
h.Set("Content-Security-Policy", cspHeader)
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
@@ -16,40 +16,71 @@ import (
|
||||
)
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
testServer := httptest.NewServer(Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Test-Header", "test value")
|
||||
_, _ = w.Write([]byte("hello world"))
|
||||
})))
|
||||
t.Cleanup(testServer.Close)
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
wrapFunc func(http.Handler) http.Handler
|
||||
expectHeaders http.Header
|
||||
}{
|
||||
{
|
||||
name: "wrap",
|
||||
wrapFunc: Wrap,
|
||||
expectHeaders: http.Header{
|
||||
"X-Test-Header": []string{"test value"},
|
||||
"Content-Security-Policy": []string{"default-src 'none'; frame-ancestors 'none'"},
|
||||
"Content-Type": []string{"text/plain; charset=utf-8"},
|
||||
"Referrer-Policy": []string{"no-referrer"},
|
||||
"X-Content-Type-Options": []string{"nosniff"},
|
||||
"X-Frame-Options": []string{"DENY"},
|
||||
"X-Xss-Protection": []string{"1; mode=block"},
|
||||
"X-Dns-Prefetch-Control": []string{"off"},
|
||||
"Cache-Control": []string{"no-cache,no-store,max-age=0,must-revalidate"},
|
||||
"Pragma": []string{"no-cache"},
|
||||
"Expires": []string{"0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom CSP",
|
||||
wrapFunc: func(h http.Handler) http.Handler { return WrapWithCustomCSP(h, "my-custom-csp-header") },
|
||||
expectHeaders: http.Header{
|
||||
"X-Test-Header": []string{"test value"},
|
||||
"Content-Security-Policy": []string{"my-custom-csp-header"},
|
||||
"Content-Type": []string{"text/plain; charset=utf-8"},
|
||||
"Referrer-Policy": []string{"no-referrer"},
|
||||
"X-Content-Type-Options": []string{"nosniff"},
|
||||
"X-Frame-Options": []string{"DENY"},
|
||||
"X-Xss-Protection": []string{"1; mode=block"},
|
||||
"X-Dns-Prefetch-Control": []string{"off"},
|
||||
"Cache-Control": []string{"no-cache,no-store,max-age=0,must-revalidate"},
|
||||
"Pragma": []string{"no-cache"},
|
||||
"Expires": []string{"0"},
|
||||
},
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testServer := httptest.NewServer(tt.wrapFunc(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Test-Header", "test value")
|
||||
_, _ = w.Write([]byte("hello world"))
|
||||
})))
|
||||
t.Cleanup(testServer.Close)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, testServer.URL, nil)
|
||||
require.NoError(t, err)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, testServer.URL, nil)
|
||||
require.NoError(t, err)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", string(respBody))
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", string(respBody))
|
||||
|
||||
expected := http.Header{
|
||||
"X-Test-Header": []string{"test value"},
|
||||
"Content-Security-Policy": []string{"default-src 'none'; frame-ancestors 'none'"},
|
||||
"Content-Type": []string{"text/plain; charset=utf-8"},
|
||||
"Referrer-Policy": []string{"no-referrer"},
|
||||
"X-Content-Type-Options": []string{"nosniff"},
|
||||
"X-Frame-Options": []string{"DENY"},
|
||||
"X-Xss-Protection": []string{"1; mode=block"},
|
||||
"X-Dns-Prefetch-Control": []string{"off"},
|
||||
"Cache-Control": []string{"no-cache,no-store,max-age=0,must-revalidate"},
|
||||
"Pragma": []string{"no-cache"},
|
||||
"Expires": []string{"0"},
|
||||
}
|
||||
for key, values := range expected {
|
||||
assert.Equalf(t, values, resp.Header.Values(key), "unexpected values for header %s", key)
|
||||
for key, values := range tt.expectHeaders {
|
||||
assert.Equalf(t, values, resp.Header.Values(key), "unexpected values for header %s", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"go.pinniped.dev/internal/httputil/securityheader"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||
@@ -109,18 +110,11 @@ func handleAuthRequestForLDAPUpstream(
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
openIDSession := &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Subject: downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse),
|
||||
RequestedAt: now,
|
||||
AuthTime: now,
|
||||
},
|
||||
}
|
||||
openIDSession.Claims.Extra = map[string]interface{}{
|
||||
oidc.DownstreamUsernameClaim: authenticateResponse.User.GetName(),
|
||||
oidc.DownstreamGroupsClaim: authenticateResponse.User.GetGroups(),
|
||||
}
|
||||
openIDSession := downstreamsession.MakeDownstreamSession(
|
||||
downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse),
|
||||
authenticateResponse.User.GetName(),
|
||||
authenticateResponse.User.GetGroups(),
|
||||
)
|
||||
|
||||
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
||||
if err != nil {
|
||||
@@ -130,6 +124,7 @@ func handleAuthRequestForLDAPUpstream(
|
||||
}
|
||||
|
||||
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -236,18 +231,14 @@ func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fos
|
||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||
return nil, false
|
||||
}
|
||||
grantScopes(authorizeRequester)
|
||||
return authorizeRequester, true
|
||||
}
|
||||
|
||||
func grantScopes(authorizeRequester fosite.AuthorizeRequester) {
|
||||
// Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested.
|
||||
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||
// There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope
|
||||
// at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess)
|
||||
// Grant the pinniped:request-audience scope if requested.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience")
|
||||
downstreamsession.GrantScopesIfRequested(authorizeRequester)
|
||||
|
||||
return authorizeRequester, true
|
||||
}
|
||||
|
||||
func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
|
||||
|
||||
@@ -1156,7 +1156,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords)
|
||||
case test.wantRedirectLocationRegexp != "":
|
||||
require.Len(t, rsp.Header().Values("Location"), 1)
|
||||
oidctestutil.RequireAuthcodeRedirectLocation(
|
||||
oidctestutil.RequireAuthCodeRegexpMatch(
|
||||
t,
|
||||
rsp.Header().Get("Location"),
|
||||
test.wantRedirectLocationRegexp,
|
||||
|
||||
@@ -9,18 +9,16 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
|
||||
"go.pinniped.dev/internal/httputil/httperr"
|
||||
"go.pinniped.dev/internal/httputil/securityheader"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
@@ -38,7 +36,7 @@ func NewHandler(
|
||||
stateDecoder, cookieDecoder oidc.Decoder,
|
||||
redirectURI string,
|
||||
) http.Handler {
|
||||
return securityheader.Wrap(httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
handler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
state, err := validateRequest(r, stateDecoder, cookieDecoder)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -65,9 +63,7 @@ func NewHandler(
|
||||
}
|
||||
|
||||
// Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested.
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess)
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience")
|
||||
downstreamsession.GrantScopesIfRequested(authorizeRequester)
|
||||
|
||||
token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens(
|
||||
r.Context(),
|
||||
@@ -91,7 +87,8 @@ func NewHandler(
|
||||
return err
|
||||
}
|
||||
|
||||
openIDSession := makeDownstreamSession(subject, username, groups)
|
||||
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups)
|
||||
|
||||
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
||||
if err != nil {
|
||||
plog.WarningErr("error while generating and saving authcode", err, "upstreamName", upstreamIDPConfig.GetName())
|
||||
@@ -101,7 +98,8 @@ func NewHandler(
|
||||
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
|
||||
|
||||
return nil
|
||||
}))
|
||||
})
|
||||
return securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
|
||||
}
|
||||
|
||||
func authcode(r *http.Request) string {
|
||||
@@ -347,22 +345,3 @@ func extractGroups(groupsAsInterface interface{}) ([]string, bool) {
|
||||
|
||||
return groupsAsStrings, true
|
||||
}
|
||||
|
||||
func makeDownstreamSession(subject string, username string, groups []string) *openid.DefaultSession {
|
||||
now := time.Now().UTC()
|
||||
openIDSession := &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Subject: subject,
|
||||
RequestedAt: now,
|
||||
AuthTime: now,
|
||||
},
|
||||
}
|
||||
if groups == nil {
|
||||
groups = []string{}
|
||||
}
|
||||
openIDSession.Claims.Extra = map[string]interface{}{
|
||||
oidc.DownstreamUsernameClaim: username,
|
||||
oidc.DownstreamGroupsClaim: groups,
|
||||
}
|
||||
return openIDSession
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
wantContentType string
|
||||
wantBody string
|
||||
wantRedirectLocationRegexp string
|
||||
wantBodyFormResponseRegexp string
|
||||
wantDownstreamGrantedScopes []string
|
||||
wantDownstreamIDTokenSubject string
|
||||
wantDownstreamIDTokenUsername string
|
||||
@@ -133,6 +134,32 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
|
||||
wantExchangeAndValidateTokensCall *oidctestutil.ExchangeAuthcodeAndValidateTokenArgs
|
||||
}{
|
||||
{
|
||||
name: "GET with good state and cookie and successful upstream token exchange with response_mode=form_post returns 200 with HTML+JS form",
|
||||
idp: happyUpstream().Build(),
|
||||
method: http.MethodGet,
|
||||
path: newRequestPath().WithState(
|
||||
happyUpstreamStateParam().WithAuthorizeRequestParams(
|
||||
shallowCopyAndModifyQuery(
|
||||
happyDownstreamRequestParamsQuery,
|
||||
map[string]string{"response_mode": "form_post"},
|
||||
).Encode(),
|
||||
).Build(t, happyStateCodec),
|
||||
).String(),
|
||||
csrfCookie: happyCSRFCookie,
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html;charset=UTF-8",
|
||||
wantBodyFormResponseRegexp: `<code id="manual-auth-code">(.+)</code>`,
|
||||
wantDownstreamIDTokenSubject: upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject,
|
||||
wantDownstreamIDTokenUsername: upstreamUsername,
|
||||
wantDownstreamIDTokenGroups: upstreamGroupMembership,
|
||||
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||
wantDownstreamNonce: downstreamNonce,
|
||||
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||
wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs,
|
||||
},
|
||||
{
|
||||
name: "GET with good state and cookie and successful upstream token exchange returns 302 to downstream client callback with its state and code",
|
||||
idp: happyUpstream().Build(),
|
||||
@@ -666,15 +693,40 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
require.Equal(t, test.wantStatus, rsp.Code)
|
||||
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType)
|
||||
|
||||
if test.wantBody != "" {
|
||||
switch {
|
||||
// If we want a specific static response body, assert that.
|
||||
case test.wantBody != "":
|
||||
require.Equal(t, test.wantBody, rsp.Body.String())
|
||||
} else {
|
||||
|
||||
// Else if we want a body that contains a regex-matched auth code, assert that (for "response_mode=form_post").
|
||||
case test.wantBodyFormResponseRegexp != "":
|
||||
oidctestutil.RequireAuthCodeRegexpMatch(
|
||||
t,
|
||||
rsp.Body.String(),
|
||||
test.wantBodyFormResponseRegexp,
|
||||
client,
|
||||
secrets,
|
||||
oauthStore,
|
||||
test.wantDownstreamGrantedScopes,
|
||||
test.wantDownstreamIDTokenSubject,
|
||||
test.wantDownstreamIDTokenUsername,
|
||||
test.wantDownstreamIDTokenGroups,
|
||||
test.wantDownstreamRequestedScopes,
|
||||
test.wantDownstreamPKCEChallenge,
|
||||
test.wantDownstreamPKCEChallengeMethod,
|
||||
test.wantDownstreamNonce,
|
||||
downstreamClientID,
|
||||
downstreamRedirectURI,
|
||||
)
|
||||
|
||||
// Otherwise, expect an empty response body.
|
||||
default:
|
||||
require.Empty(t, rsp.Body.String())
|
||||
}
|
||||
|
||||
if test.wantRedirectLocationRegexp != "" { //nolint:nestif // don't mind have several sequential if statements in this test
|
||||
require.Len(t, rsp.Header().Values("Location"), 1)
|
||||
oidctestutil.RequireAuthcodeRedirectLocation(
|
||||
oidctestutil.RequireAuthCodeRegexpMatch(
|
||||
t,
|
||||
rsp.Header().Get("Location"),
|
||||
test.wantRedirectLocationRegexp,
|
||||
|
||||
100
internal/oidc/clientregistry/clientregistry.go
Normal file
100
internal/oidc/clientregistry/clientregistry.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package clientregistry defines Pinniped's OAuth2/OIDC clients.
|
||||
package clientregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/ory/fosite"
|
||||
)
|
||||
|
||||
// Client represents a Pinniped OAuth/OIDC client.
|
||||
type Client struct {
|
||||
fosite.DefaultOpenIDConnectClient
|
||||
}
|
||||
|
||||
func (c Client) GetResponseModes() []fosite.ResponseModeType {
|
||||
// For now, all Pinniped clients always support "" (unspecified), "query", and "form_post" response modes.
|
||||
return []fosite.ResponseModeType{fosite.ResponseModeDefault, fosite.ResponseModeQuery, fosite.ResponseModeFormPost}
|
||||
}
|
||||
|
||||
// It implements both the base, OIDC, and response_mode client interfaces of Fosite.
|
||||
var (
|
||||
_ fosite.Client = (*Client)(nil)
|
||||
_ fosite.OpenIDConnectClient = (*Client)(nil)
|
||||
_ fosite.ResponseModeClient = (*Client)(nil)
|
||||
)
|
||||
|
||||
// StaticClientManager is a fosite.ClientManager with statically-defined clients.
|
||||
type StaticClientManager struct{}
|
||||
|
||||
var _ fosite.ClientManager = (*StaticClientManager)(nil)
|
||||
|
||||
// GetClient returns a static client specified by the given ID.
|
||||
//
|
||||
// It returns a fosite.ErrNotFound if an unknown client is specified.
|
||||
func (StaticClientManager) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||
switch id {
|
||||
case "pinniped-cli":
|
||||
return PinnipedCLI(), nil
|
||||
default:
|
||||
return nil, fosite.ErrNotFound.WithDescription("no such client")
|
||||
}
|
||||
}
|
||||
|
||||
// ClientAssertionJWTValid returns an error if the JTI is
|
||||
// known or the DB check failed and nil if the JTI is not known.
|
||||
//
|
||||
// This functionality is not supported by the StaticClientManager.
|
||||
func (StaticClientManager) ClientAssertionJWTValid(ctx context.Context, jti string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// SetClientAssertionJWT marks a JTI as known for the given
|
||||
// expiry time. Before inserting the new JTI, it will clean
|
||||
// up any existing JTIs that have expired as those tokens can
|
||||
// not be replayed due to the expiry.
|
||||
//
|
||||
// This functionality is not supported by the StaticClientManager.
|
||||
func (StaticClientManager) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// PinnipedCLI returns the static Client corresponding to the Pinniped CLI.
|
||||
func PinnipedCLI() *Client {
|
||||
return &Client{
|
||||
DefaultOpenIDConnectClient: fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinniped-cli",
|
||||
Secret: nil,
|
||||
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
||||
GrantTypes: fosite.Arguments{
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
},
|
||||
ResponseTypes: []string{"code"},
|
||||
Scopes: fosite.Arguments{
|
||||
oidc.ScopeOpenID,
|
||||
oidc.ScopeOfflineAccess,
|
||||
"profile",
|
||||
"email",
|
||||
"pinniped:request-audience",
|
||||
},
|
||||
Audience: nil,
|
||||
Public: true,
|
||||
},
|
||||
RequestURIs: nil,
|
||||
JSONWebKeys: nil,
|
||||
JSONWebKeysURI: "",
|
||||
RequestObjectSigningAlgorithm: "",
|
||||
TokenEndpointAuthSigningAlgorithm: oidc.RS256,
|
||||
TokenEndpointAuthMethod: "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
96
internal/oidc/clientregistry/clientregistry_test.go
Normal file
96
internal/oidc/clientregistry/clientregistry_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package clientregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStaticRegistry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("unimplemented methods", func(t *testing.T) {
|
||||
registry := StaticClientManager{}
|
||||
require.EqualError(t, registry.ClientAssertionJWTValid(ctx, "some-token-id"), "not implemented")
|
||||
require.EqualError(t, registry.SetClientAssertionJWT(ctx, "some-token-id", time.Now()), "not implemented")
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
registry := StaticClientManager{}
|
||||
got, err := registry.GetClient(ctx, "does-not-exist")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, got)
|
||||
rfcErr := fosite.ErrorToRFC6749Error(err)
|
||||
require.NotNil(t, rfcErr)
|
||||
require.Equal(t, rfcErr.CodeField, 404)
|
||||
require.Equal(t, rfcErr.GetDescription(), "no such client")
|
||||
})
|
||||
|
||||
t.Run("pinniped CLI", func(t *testing.T) {
|
||||
registry := StaticClientManager{}
|
||||
got, err := registry.GetClient(ctx, "pinniped-cli")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
require.IsType(t, &Client{}, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPinnipedCLI(t *testing.T) {
|
||||
c := PinnipedCLI()
|
||||
require.Equal(t, "pinniped-cli", c.GetID())
|
||||
require.Nil(t, c.GetHashedSecret())
|
||||
require.Equal(t, []string{"http://127.0.0.1/callback"}, c.GetRedirectURIs())
|
||||
require.Equal(t, fosite.Arguments{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"}, c.GetGrantTypes())
|
||||
require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes())
|
||||
require.Equal(t, fosite.Arguments{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience"}, c.GetScopes())
|
||||
require.True(t, c.IsPublic())
|
||||
require.Nil(t, c.GetAudience())
|
||||
require.Nil(t, c.GetRequestURIs())
|
||||
require.Nil(t, c.GetJSONWebKeys())
|
||||
require.Equal(t, "", c.GetJSONWebKeysURI())
|
||||
require.Equal(t, "", c.GetRequestObjectSigningAlgorithm())
|
||||
require.Equal(t, "none", c.GetTokenEndpointAuthMethod())
|
||||
require.Equal(t, "RS256", c.GetTokenEndpointAuthSigningAlgorithm())
|
||||
require.Equal(t, []fosite.ResponseModeType{"", "query", "form_post"}, c.GetResponseModes())
|
||||
|
||||
marshaled, err := json.Marshal(c)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `
|
||||
{
|
||||
"id": "pinniped-cli",
|
||||
"redirect_uris": [
|
||||
"http://127.0.0.1/callback"
|
||||
],
|
||||
"grant_types": [
|
||||
"authorization_code",
|
||||
"refresh_token",
|
||||
"urn:ietf:params:oauth:grant-type:token-exchange"
|
||||
],
|
||||
"response_types": [
|
||||
"code"
|
||||
],
|
||||
"scopes": [
|
||||
"openid",
|
||||
"offline_access",
|
||||
"profile",
|
||||
"email",
|
||||
"pinniped:request-audience"
|
||||
],
|
||||
"audience": null,
|
||||
"public": true,
|
||||
"jwks_uri": "",
|
||||
"jwks": null,
|
||||
"token_endpoint_auth_method": "none",
|
||||
"request_uris": null,
|
||||
"request_object_signing_alg": "",
|
||||
"token_endpoint_auth_signing_alg": "RS256"
|
||||
}`, string(marshaled))
|
||||
}
|
||||
@@ -25,6 +25,7 @@ type Metadata struct {
|
||||
JWKSURI string `json:"jwks_uri"`
|
||||
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
ResponseModesSupported []string `json:"response_modes_supported"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||
|
||||
@@ -63,6 +64,7 @@ func NewHandler(issuerURL string) http.Handler {
|
||||
JWKSURI: issuerURL + oidc.JWKSEndpointPath,
|
||||
SupervisorDiscovery: SupervisorDiscoveryMetadataV1Alpha1{PinnipedIDPsEndpoint: issuerURL + oidc.PinnipedIDPsPathV1Alpha1},
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
ResponseModesSupported: []string{"query", "form_post"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IDTokenSigningAlgValuesSupported: []string{"ES256"},
|
||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestDiscovery(t *testing.T) {
|
||||
PinnipedIDPsEndpoint: "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers",
|
||||
},
|
||||
ResponseTypesSupported: []string{"code"},
|
||||
ResponseModesSupported: []string{"query", "form_post"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IDTokenSigningAlgValuesSupported: []string{"ES256"},
|
||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
|
||||
|
||||
43
internal/oidc/downstreamsession/downstream_session.go
Normal file
43
internal/oidc/downstreamsession/downstream_session.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package downstreamsession provides some shared helpers for creating downstream OIDC sessions.
|
||||
package downstreamsession
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
oidc2 "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
)
|
||||
|
||||
// MakeDownstreamSession creates a downstream OIDC session.
|
||||
func MakeDownstreamSession(subject string, username string, groups []string) *openid.DefaultSession {
|
||||
now := time.Now().UTC()
|
||||
openIDSession := &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Subject: subject,
|
||||
RequestedAt: now,
|
||||
AuthTime: now,
|
||||
},
|
||||
}
|
||||
if groups == nil {
|
||||
groups = []string{}
|
||||
}
|
||||
openIDSession.Claims.Extra = map[string]interface{}{
|
||||
oidc.DownstreamUsernameClaim: username,
|
||||
oidc.DownstreamGroupsClaim: groups,
|
||||
}
|
||||
return openIDSession
|
||||
}
|
||||
|
||||
// GrantScopesIfRequested auto-grants the scopes for which we do not require end-user approval, if they were requested.
|
||||
func GrantScopesIfRequested(authorizeRequester fosite.AuthorizeRequester) {
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, oidc2.ScopeOpenID)
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, oidc2.ScopeOfflineAccess)
|
||||
oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience")
|
||||
}
|
||||
@@ -13,18 +13,17 @@ import (
|
||||
fositepkce "github.com/ory/fosite/handler/pkce"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/fositestorage/accesstoken"
|
||||
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
||||
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
||||
"go.pinniped.dev/internal/fositestorage/pkce"
|
||||
"go.pinniped.dev/internal/fositestorage/refreshtoken"
|
||||
"go.pinniped.dev/internal/fositestoragei"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const errKubeStorageNotImplemented = constable.Error("KubeStorage does not implement this method. It should not have been called.")
|
||||
|
||||
type KubeStorage struct {
|
||||
clientManager fosite.ClientManager
|
||||
authorizationCodeStorage oauth2.AuthorizeCodeStorage
|
||||
pkceStorage fositepkce.PKCERequestStorage
|
||||
oidcStorage openid.OpenIDConnectRequestStorage
|
||||
@@ -37,6 +36,7 @@ var _ fositestoragei.AllFositeStorage = &KubeStorage{}
|
||||
func NewKubeStorage(secrets corev1client.SecretInterface, timeoutsConfiguration TimeoutsConfiguration) *KubeStorage {
|
||||
nowFunc := time.Now
|
||||
return &KubeStorage{
|
||||
clientManager: &clientregistry.StaticClientManager{},
|
||||
authorizationCodeStorage: authorizationcode.New(secrets, nowFunc, timeoutsConfiguration.AuthorizationCodeSessionStorageLifetime),
|
||||
pkceStorage: pkce.New(secrets, nowFunc, timeoutsConfiguration.PKCESessionStorageLifetime),
|
||||
oidcStorage: openidconnect.New(secrets, nowFunc, timeoutsConfiguration.OIDCSessionStorageLifetime),
|
||||
@@ -183,26 +183,15 @@ func (k KubeStorage) RevokeRefreshToken(ctx context.Context, requestID string) e
|
||||
//
|
||||
// OAuth client definitions:
|
||||
//
|
||||
// For the time being, we only allow a single pre-defined client, so we do not need to interact with any underlying
|
||||
// storage mechanism to fetch them.
|
||||
//
|
||||
|
||||
func (KubeStorage) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||
client := PinnipedCLIOIDCClient()
|
||||
if client.ID == id {
|
||||
return client, nil
|
||||
}
|
||||
return nil, fosite.ErrNotFound
|
||||
func (k KubeStorage) GetClient(ctx context.Context, id string) (fosite.Client, error) {
|
||||
return k.clientManager.GetClient(ctx, id)
|
||||
}
|
||||
|
||||
//
|
||||
// Unused interface methods.
|
||||
//
|
||||
|
||||
func (KubeStorage) ClientAssertionJWTValid(_ context.Context, _ string) error {
|
||||
return errKubeStorageNotImplemented
|
||||
func (k KubeStorage) ClientAssertionJWTValid(ctx context.Context, jti string) error {
|
||||
return k.clientManager.ClientAssertionJWTValid(ctx, jti)
|
||||
}
|
||||
|
||||
func (KubeStorage) SetClientAssertionJWT(_ context.Context, _ string, _ time.Time) error {
|
||||
return errKubeStorageNotImplemented
|
||||
func (k KubeStorage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error {
|
||||
return k.clientManager.SetClientAssertionJWT(ctx, jti, exp)
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/fositestoragei"
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const errNullStorageNotImplemented = constable.Error("NullStorage does not implement this method. It should not have been called.")
|
||||
|
||||
type NullStorage struct{}
|
||||
type NullStorage struct {
|
||||
clientregistry.StaticClientManager
|
||||
}
|
||||
|
||||
var _ fositestoragei.AllFositeStorage = &NullStorage{}
|
||||
|
||||
@@ -86,19 +88,3 @@ func (NullStorage) GetAuthorizeCodeSession(_ context.Context, _ string, _ fosite
|
||||
func (NullStorage) InvalidateAuthorizeCodeSession(_ context.Context, _ string) (err error) {
|
||||
return errNullStorageNotImplemented
|
||||
}
|
||||
|
||||
func (NullStorage) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||
client := PinnipedCLIOIDCClient()
|
||||
if client.ID == id {
|
||||
return client, nil
|
||||
}
|
||||
return nil, fosite.ErrNotFound
|
||||
}
|
||||
|
||||
func (NullStorage) ClientAssertionJWTValid(_ context.Context, _ string) error {
|
||||
return errNullStorageNotImplemented
|
||||
}
|
||||
|
||||
func (NullStorage) SetClientAssertionJWT(_ context.Context, _ string, _ time.Time) error {
|
||||
return errNullStorageNotImplemented
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNullStorage_GetClient(t *testing.T) {
|
||||
storage := NullStorage{}
|
||||
|
||||
client, err := storage.GetClient(context.Background(), "some-other-client")
|
||||
require.Equal(t, fosite.ErrNotFound, err)
|
||||
require.Zero(t, client)
|
||||
|
||||
client, err = storage.GetClient(context.Background(), "pinniped-cli")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
&fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinniped-cli",
|
||||
Public: true,
|
||||
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
||||
ResponseTypes: []string{"code"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
||||
Scopes: []string{"openid", "offline_access", "profile", "email", "pinniped:request-audience"},
|
||||
},
|
||||
TokenEndpointAuthMethod: "none",
|
||||
},
|
||||
client,
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||
"go.pinniped.dev/internal/oidc/jwks"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||
)
|
||||
@@ -98,20 +99,6 @@ type UpstreamStateParamData struct {
|
||||
FormatVersion string `json:"v"`
|
||||
}
|
||||
|
||||
func PinnipedCLIOIDCClient() *fosite.DefaultOpenIDConnectClient {
|
||||
return &fosite.DefaultOpenIDConnectClient{
|
||||
DefaultClient: &fosite.DefaultClient{
|
||||
ID: "pinniped-cli",
|
||||
Public: true,
|
||||
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
||||
ResponseTypes: []string{"code"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
||||
Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email", "pinniped:request-audience"},
|
||||
},
|
||||
TokenEndpointAuthMethod: "none",
|
||||
}
|
||||
}
|
||||
|
||||
type TimeoutsConfiguration struct {
|
||||
// The length of time that our state param that we encrypt and pass to the upstream OIDC IDP should be considered
|
||||
// valid. If a state param generated by the authorize endpoint is sent to the callback endpoint after this much
|
||||
@@ -231,7 +218,7 @@ func FositeOauth2Helper(
|
||||
MinParameterEntropy: fosite.MinParameterEntropy,
|
||||
}
|
||||
|
||||
return compose.Compose(
|
||||
provider := compose.Compose(
|
||||
oauthConfig,
|
||||
oauthStore,
|
||||
&compose.CommonStrategy{
|
||||
@@ -247,6 +234,8 @@ func FositeOauth2Helper(
|
||||
compose.OAuth2PKCEFactory,
|
||||
TokenExchangeFactory,
|
||||
)
|
||||
provider.(*fosite.Fosite).FormPostHTMLTemplate = formposthtml.Template()
|
||||
return provider
|
||||
}
|
||||
|
||||
// FositeErrorForLog generates a list of information about the provided Fosite error that can be
|
||||
|
||||
87
internal/oidc/provider/formposthtml/form_post.css
Normal file
87
internal/oidc/provider/formposthtml/form_post.css
Normal file
@@ -0,0 +1,87 @@
|
||||
/* Copyright 2021 the Pinniped contributors. All Rights Reserved. */
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
body {
|
||||
font-family: "Metropolis-Light", Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.state {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
width: 400px;
|
||||
height: 80px;
|
||||
margin-top: -40px;
|
||||
margin-left: -200px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: -10px;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
display: inline;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: all .1s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #eee;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #ddd;
|
||||
transform: scale(.99);
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
float: left;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-top: -3px;
|
||||
margin-right: 10px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
/*
|
||||
This is the "copy-to-clipboard-line.svg" icon from Clarity (https://clarity.design/):
|
||||
https://github.com/vmware/clarity-assets/blob/master/icons/essential/copy-to-clipboard-line.svg
|
||||
*/
|
||||
background-image: url("data:image/svg+xml,%3Csvg version='1.1' width='36' height='36' viewBox='0 0 36 36' preserveAspectRatio='xMidYMid meet' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Ecopy-to-clipboard-line%3C/title%3E%3Cpath d='M22.6,4H21.55a3.89,3.89,0,0,0-7.31,0H13.4A2.41,2.41,0,0,0,11,6.4V10H25V6.4A2.41,2.41,0,0,0,22.6,4ZM23,8H13V6.25A.25.25,0,0,1,13.25,6h2.69l.12-1.11A1.24,1.24,0,0,1,16.61,4a2,2,0,0,1,3.15,1.18l.09.84h2.9a.25.25,0,0,1,.25.25Z' class='clr-i-outline clr-i-outline-path-1'%3E%3C/path%3E%3Cpath d='M33.25,18.06H21.33l2.84-2.83a1,1,0,1,0-1.42-1.42L17.5,19.06l5.25,5.25a1,1,0,0,0,.71.29,1,1,0,0,0,.71-1.7l-2.84-2.84H33.25a1,1,0,0,0,0-2Z' class='clr-i-outline clr-i-outline-path-2'%3E%3C/path%3E%3Cpath d='M29,16h2V6.68A1.66,1.66,0,0,0,29.35,5H27.08V7H29Z' class='clr-i-outline clr-i-outline-path-3'%3E%3C/path%3E%3Cpath d='M29,31H7V7H9V5H6.64A1.66,1.66,0,0,0,5,6.67V31.32A1.66,1.66,0,0,0,6.65,33H29.36A1.66,1.66,0,0,0,31,31.33V22.06H29Z' class='clr-i-outline clr-i-outline-path-4'%3E%3C/path%3E%3Crect x='0' y='0' width='36' height='36' fill-opacity='0'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#loading {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-top: -40px;
|
||||
margin-left: -40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
border-top-color: #1b3951;
|
||||
animation: loader .6s linear infinite;
|
||||
}
|
||||
34
internal/oidc/provider/formposthtml/form_post.gohtml
Normal file
34
internal/oidc/provider/formposthtml/form_post.gohtml
Normal file
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
--><!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{{ minifiedCSS }}</style>
|
||||
<script>{{ minifiedJS }}</script>
|
||||
<link id="favicon" rel="icon"/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
To finish logging in, paste this authorization code into your command-line session: {{ .Parameters.Get "code" }}
|
||||
</noscript>
|
||||
<form>
|
||||
<input type="hidden" name="redirect_uri" value="{{ .RedirURL }}"/>
|
||||
<input type="hidden" name="encoded_params" value="{{ .Parameters.Encode }}"/>
|
||||
</form>
|
||||
<div id="loading" class="state" data-favicon="⏳" data-title="Logging in..." hidden></div>
|
||||
<div id="success" class="state" data-favicon="✅" data-title="Login succeeded" hidden>
|
||||
<h1>Login succeeded</h1>
|
||||
<p>You have successfully logged in. You may now close this tab.</p>
|
||||
</div>
|
||||
<div id="manual" class="state" data-favicon="⌛" data-title="Finish your login" hidden>
|
||||
<h1>Finish your login</h1>
|
||||
<p>To finish logging in, paste this authorization code into your command-line session:</p>
|
||||
<button id="manual-copy-button">
|
||||
<span class="copy-icon"></span>
|
||||
<code id="manual-auth-code">{{ .Parameters.Get "code" }}</code>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
54
internal/oidc/provider/formposthtml/form_post.js
Normal file
54
internal/oidc/provider/formposthtml/form_post.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
window.onload = () => {
|
||||
const transitionToState = (id) => {
|
||||
// Hide all the other ".state" <div>s.
|
||||
Array.from(document.querySelectorAll('.state')).forEach(e => e.hidden = true);
|
||||
|
||||
// Unhide the current state <div>.
|
||||
const currentDiv = document.getElementById(id)
|
||||
currentDiv.hidden = false;
|
||||
|
||||
// Set the window title.
|
||||
document.title = currentDiv.dataset.title;
|
||||
|
||||
// Set the favicon using inline SVG (does not work on Safari).
|
||||
document.getElementById('favicon').setAttribute(
|
||||
'href',
|
||||
'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>' +
|
||||
currentDiv.dataset.favicon +
|
||||
'</text></svg>'
|
||||
);
|
||||
}
|
||||
|
||||
// At load, show the spinner, hide the other divs, set the favicon, and
|
||||
// replace the URL path with './' so the upstream auth code disappears.
|
||||
transitionToState('loading');
|
||||
window.history.replaceState(null, '', './');
|
||||
|
||||
// When the copy button is clicked, copy to the clipboard.
|
||||
document.getElementById('manual-copy-button').onclick = () => {
|
||||
const code = document.getElementById('manual-copy-button').innerText;
|
||||
navigator.clipboard.writeText(code)
|
||||
.then(() => console.info('copied authorization code ' + code + ' to clipboard'))
|
||||
.catch(e => console.error('failed to copy code ' + code + ' to clipboard: ' + e));
|
||||
};
|
||||
|
||||
// Set a timeout to transition to the "manual" state if nothing succeeds within 2s.
|
||||
const timeout = setTimeout(() => transitionToState('manual'), 2000);
|
||||
|
||||
// Try to submit the POST callback, handling the success and error cases.
|
||||
const responseParams = document.forms[0].elements;
|
||||
fetch(
|
||||
responseParams['redirect_uri'].value,
|
||||
{
|
||||
method: 'POST',
|
||||
mode: 'no-cors',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},
|
||||
body: responseParams['encoded_params'].value,
|
||||
})
|
||||
.then(() => clearTimeout(timeout))
|
||||
.then(() => transitionToState('success'))
|
||||
.catch(() => transitionToState('manual'));
|
||||
};
|
||||
65
internal/oidc/provider/formposthtml/formposthtml.go
Normal file
65
internal/oidc/provider/formposthtml/formposthtml.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package formposthtml defines HTML templates used by the Supervisor.
|
||||
//nolint: gochecknoglobals // This package uses globals to ensure that all parsing and minifying happens at init.
|
||||
package formposthtml
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
_ "embed" // Needed to trigger //go:embed directives below.
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/minify/v2/minify"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed form_post.css
|
||||
rawCSS string
|
||||
minifiedCSS = mustMinify(minify.CSS(rawCSS))
|
||||
|
||||
//go:embed form_post.js
|
||||
rawJS string
|
||||
minifiedJS = mustMinify(minify.JS(rawJS))
|
||||
|
||||
//go:embed form_post.gohtml
|
||||
rawHTMLTemplate string
|
||||
)
|
||||
|
||||
// Parse the Go templated HTML and inject functions providing the minified inline CSS and JS.
|
||||
var parsedHTMLTemplate = template.Must(template.New("form_post.gohtml").Funcs(template.FuncMap{
|
||||
"minifiedCSS": func() template.CSS { return template.CSS(minifiedCSS) },
|
||||
"minifiedJS": func() template.JS { return template.JS(minifiedJS) }, //nolint:gosec // This is 100% static input, not attacker-controlled.
|
||||
}).Parse(rawHTMLTemplate))
|
||||
|
||||
// Generate the CSP header value once since it's effectively constant:
|
||||
var cspValue = strings.Join([]string{
|
||||
`default-src 'none'`,
|
||||
`script-src '` + cspHash(minifiedJS) + `'`,
|
||||
`style-src '` + cspHash(minifiedCSS) + `'`,
|
||||
`img-src data:`,
|
||||
`connect-src *`,
|
||||
`frame-ancestors 'none'`,
|
||||
}, "; ")
|
||||
|
||||
func mustMinify(s string, err error) string {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func cspHash(s string) string {
|
||||
hashBytes := sha256.Sum256([]byte(s))
|
||||
return "sha256-" + base64.StdEncoding.EncodeToString(hashBytes[:])
|
||||
}
|
||||
|
||||
// ContentSecurityPolicy returns the Content-Security-Policy header value to make the Template() operate correctly.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src#:~:text=%27%3Chash-algorithm%3E-%3Cbase64-value%3E%27.
|
||||
func ContentSecurityPolicy() string { return cspValue }
|
||||
|
||||
// Template returns the html/template.Template for rendering the response_type=form_post response page.
|
||||
func Template() *template.Template { return parsedHTMLTemplate }
|
||||
101
internal/oidc/provider/formposthtml/formposthtml_test.go
Normal file
101
internal/oidc/provider/formposthtml/formposthtml_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package formposthtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
var (
|
||||
testRedirectURL = "http://127.0.0.1:12345/callback"
|
||||
|
||||
testResponseParams = url.Values{
|
||||
"code": []string{"test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU"},
|
||||
"scope": []string{"openid offline_access pinniped:request-audience"},
|
||||
"state": []string{"01234567890123456789012345678901"},
|
||||
}
|
||||
|
||||
testExpectedFormPostOutput = here.Doc(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>body{font-family:metropolis-light,Helvetica,sans-serif}h1{font-size:20px}.state{position:absolute;top:100px;left:50%;width:400px;height:80px;margin-top:-40px;margin-left:-200px;font-size:14px;line-height:24px}button{margin:-10px;padding:10px;text-align:left;width:100%;display:inline;border:none;background:0 0;cursor:pointer;transition:all .1s}button:hover{background-color:#eee;transform:scale(1.01)}button:active{background-color:#ddd;transform:scale(.99)}code{display:block;word-wrap:break-word;word-break:break-all;font-size:12px;font-family:monospace;color:#333}.copy-icon{float:left;width:36px;height:36px;margin-top:-3px;margin-right:10px;background-size:contain;background-repeat:no-repeat;background-image:url("data:image/svg+xml,%3Csvg width=%2236%22 height=%2236%22 viewBox=%220 0 36 36%22 xmlns=%22http://www.w3.org/2000/svg%22 xmlns:xlink=%22http://www.w3.org/1999/xlink%22%3E%3Ctitle%3Ecopy-to-clipboard-line%3C/title%3E%3Cpath d=%22M22.6 4H21.55a3.89 3.89.0 00-7.31.0H13.4A2.41 2.41.0 0011 6.4V10H25V6.4A2.41 2.41.0 0022.6 4zM23 8H13V6.25A.25.25.0 0113.25 6h2.69l.12-1.11A1.24 1.24.0 0116.61 4a2 2 0 013.15 1.18l.09.84h2.9a.25.25.0 01.25.25z%22 class=%22clr-i-outline clr-i-outline-path-1%22/%3E%3Cpath d=%22M33.25 18.06H21.33l2.84-2.83a1 1 0 10-1.42-1.42L17.5 19.06l5.25 5.25a1 1 0 00.71.29 1 1 0 00.71-1.7l-2.84-2.84H33.25a1 1 0 000-2z%22 class=%22clr-i-outline clr-i-outline-path-2%22/%3E%3Cpath d=%22M29 16h2V6.68A1.66 1.66.0 0029.35 5H27.08V7H29z%22 class=%22clr-i-outline clr-i-outline-path-3%22/%3E%3Cpath d=%22M29 31H7V7H9V5H6.64A1.66 1.66.0 005 6.67V31.32A1.66 1.66.0 006.65 33H29.36A1.66 1.66.0 0031 31.33V22.06H29z%22 class=%22clr-i-outline clr-i-outline-path-4%22/%3E%3Crect x=%220%22 y=%220%22 width=%2236%22 height=%2236%22 fill-opacity=%220%22/%3E%3C/svg%3E")}@keyframes loader{to{transform:rotate(360deg)}}#loading{content:'';box-sizing:border-box;width:80px;height:80px;margin-top:-40px;margin-left:-40px;border-radius:50%;border:2px solid #fff;border-top-color:#1b3951;animation:loader .6s linear infinite}</style>
|
||||
<script>window.onload=()=>{const a=b=>{Array.from(document.querySelectorAll('.state')).forEach(a=>a.hidden=!0);const a=document.getElementById(b);a.hidden=!1,document.title=a.dataset.title,document.getElementById('favicon').setAttribute('href','data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>'+a.dataset.favicon+'</text></svg>')};a('loading'),window.history.replaceState(null,'','./'),document.getElementById('manual-copy-button').onclick=()=>{const a=document.getElementById('manual-copy-button').innerText;navigator.clipboard.writeText(a).then(()=>console.info('copied authorization code '+a+' to clipboard')).catch(b=>console.error('failed to copy code '+a+' to clipboard: '+b))};const c=setTimeout(()=>a('manual'),2e3),b=document.forms[0].elements;fetch(b.redirect_uri.value,{method:'POST',mode:'no-cors',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:b.encoded_params.value}).then(()=>clearTimeout(c)).then(()=>a('success')).catch(()=>a('manual'))}</script>
|
||||
<link id="favicon" rel="icon"/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
To finish logging in, paste this authorization code into your command-line session: test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU
|
||||
</noscript>
|
||||
<form>
|
||||
<input type="hidden" name="redirect_uri" value="http://127.0.0.1:12345/callback"/>
|
||||
<input type="hidden" name="encoded_params" value="code=test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU&scope=openid+offline_access+pinniped%3Arequest-audience&state=01234567890123456789012345678901"/>
|
||||
</form>
|
||||
<div id="loading" class="state" data-favicon="⏳" data-title="Logging in..." hidden></div>
|
||||
<div id="success" class="state" data-favicon="✅" data-title="Login succeeded" hidden>
|
||||
<h1>Login succeeded</h1>
|
||||
<p>You have successfully logged in. You may now close this tab.</p>
|
||||
</div>
|
||||
<div id="manual" class="state" data-favicon="⌛" data-title="Finish your login" hidden>
|
||||
<h1>Finish your login</h1>
|
||||
<p>To finish logging in, paste this authorization code into your command-line session:</p>
|
||||
<button id="manual-copy-button">
|
||||
<span class="copy-icon"></span>
|
||||
<code id="manual-auth-code">test-S629KHsCCBYV0PQ6FDSrn6iEXtVImQRBh7NCAk.JezyUSdCiSslYjtUmv7V5VAgiCz3ZkES9mYldg9GhqU</code>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
// It's okay if this changes in the future, but this gives us a chance to eyeball the formatting.
|
||||
// Our browser-based integration tests should find any incompatibilities.
|
||||
testExpectedCSP = `default-src 'none'; ` +
|
||||
`script-src 'sha256-U+tKnJ2oMSYKSxmSX3V2mPBN8xdr9JpampKAhbSo108='; ` +
|
||||
`style-src 'sha256-CtfkX7m8x2UdGYvGgDq+6b6yIAQsASW9pbQK+sG8fNA='; ` +
|
||||
`img-src data:; ` +
|
||||
`connect-src *; ` +
|
||||
`frame-ancestors 'none'`
|
||||
)
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
// Use the Fosite helper to render the form, ensuring that the parameters all have the same names + types.
|
||||
var buf bytes.Buffer
|
||||
fosite.WriteAuthorizeFormPostResponse(testRedirectURL, testResponseParams, Template(), &buf)
|
||||
|
||||
// Render again so we can confirm that there is no error returned (Fosite ignores any error).
|
||||
var buf2 bytes.Buffer
|
||||
require.NoError(t, Template().Execute(&buf2, struct {
|
||||
RedirURL string
|
||||
Parameters url.Values
|
||||
}{
|
||||
RedirURL: testRedirectURL,
|
||||
Parameters: testResponseParams,
|
||||
}))
|
||||
|
||||
require.Equal(t, buf.String(), buf2.String())
|
||||
require.Equal(t, testExpectedFormPostOutput, buf.String())
|
||||
}
|
||||
|
||||
func TestContentSecurityPolicyHashes(t *testing.T) {
|
||||
require.Equal(t, testExpectedCSP, ContentSecurityPolicy())
|
||||
}
|
||||
|
||||
func TestHelpers(t *testing.T) {
|
||||
// These are silly tests but it's easy to we might as well have them.
|
||||
require.Equal(t, "test", mustMinify("test", nil))
|
||||
require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) })
|
||||
|
||||
// Example test vector from https://content-security-policy.com/hash/.
|
||||
require.Equal(t, "sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=", cspHash("doSomething();"))
|
||||
}
|
||||
@@ -106,6 +106,8 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
|
||||
return failureResponse(), nil
|
||||
}
|
||||
|
||||
// this timestamp should be returned from IssueClientCertPEM but this is a safe approximation
|
||||
expires := metav1.NewTime(time.Now().UTC().Add(clientCertificateTTL))
|
||||
certPEM, keyPEM, err := r.issuer.IssueClientCertPEM(userInfo.GetName(), userInfo.GetGroups(), clientCertificateTTL)
|
||||
if err != nil {
|
||||
traceFailureWithError(t, "cert issuer", err)
|
||||
@@ -117,7 +119,7 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
|
||||
return &loginapi.TokenCredentialRequest{
|
||||
Status: loginapi.TokenCredentialRequestStatus{
|
||||
Credential: &loginapi.ClusterCredential{
|
||||
ExpirationTimestamp: metav1.NewTime(time.Now().UTC().Add(clientCertificateTTL)),
|
||||
ExpirationTimestamp: expires,
|
||||
ClientCertificateData: string(certPEM),
|
||||
ClientKeyData: string(keyPEM),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testutil
|
||||
@@ -55,7 +55,9 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret
|
||||
}
|
||||
|
||||
func RequireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) {
|
||||
require.Equal(t, "default-src 'none'; frame-ancestors 'none'", response.Header().Get("Content-Security-Policy"))
|
||||
// This is a more relaxed assertion rather than an exact match, so it can cover all the CSP headers we use.
|
||||
require.Contains(t, response.Header().Get("Content-Security-Policy"), "default-src 'none'")
|
||||
|
||||
require.Equal(t, "DENY", response.Header().Get("X-Frame-Options"))
|
||||
require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection"))
|
||||
require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options"))
|
||||
|
||||
@@ -235,10 +235,10 @@ func VerifyECDSAIDToken(
|
||||
return token
|
||||
}
|
||||
|
||||
func RequireAuthcodeRedirectLocation(
|
||||
func RequireAuthCodeRegexpMatch(
|
||||
t *testing.T,
|
||||
actualRedirectLocation string,
|
||||
wantRedirectLocationRegexp string,
|
||||
actualContent string,
|
||||
wantRegexp string,
|
||||
kubeClient *fake.Clientset,
|
||||
secretsClient v1.SecretInterface,
|
||||
oauthStore fositestoragei.AllFositeStorage,
|
||||
@@ -256,9 +256,9 @@ func RequireAuthcodeRedirectLocation(
|
||||
t.Helper()
|
||||
|
||||
// Assert that Location header matches regular expression.
|
||||
regex := regexp.MustCompile(wantRedirectLocationRegexp)
|
||||
submatches := regex.FindStringSubmatch(actualRedirectLocation)
|
||||
require.Lenf(t, submatches, 2, "no regexp match in actualRedirectLocation: %q", actualRedirectLocation)
|
||||
regex := regexp.MustCompile(wantRegexp)
|
||||
submatches := regex.FindStringSubmatch(actualContent)
|
||||
require.Lenf(t, submatches, 2, "no regexp match in actualContent: %", actualContent)
|
||||
capturedAuthCode := submatches[1]
|
||||
|
||||
// fosite authcodes are in the format `data.signature`, so grab the signature part, which is the lookup key in the storage interface
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -58,11 +60,19 @@ const (
|
||||
defaultLDAPUsernamePrompt = "Username: "
|
||||
defaultLDAPPasswordPrompt = "Password: "
|
||||
|
||||
// For CLI-based auth, such as with LDAP upstream identity providers, the user may use these environment variables
|
||||
// to avoid getting interactively prompted for username and password.
|
||||
defaultUsernameEnvVarName = "PINNIPED_USERNAME"
|
||||
defaultPasswordEnvVarName = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential
|
||||
|
||||
httpLocationHeaderName = "Location"
|
||||
|
||||
debugLogLevel = 4
|
||||
)
|
||||
|
||||
// stdin returns the file descriptor for stdin as an int.
|
||||
func stdin() int { return int(os.Stdin.Fd()) }
|
||||
|
||||
type handlerState struct {
|
||||
// Basic parameters.
|
||||
ctx context.Context
|
||||
@@ -87,6 +97,7 @@ type handlerState struct {
|
||||
// Generated parameters of a login flow.
|
||||
provider *oidc.Provider
|
||||
oauth2Config *oauth2.Config
|
||||
useFormPost bool
|
||||
state state.State
|
||||
nonce nonce.Nonce
|
||||
pkce pkce.Code
|
||||
@@ -96,9 +107,12 @@ type handlerState struct {
|
||||
generatePKCE func() (pkce.Code, error)
|
||||
generateNonce func() (nonce.Nonce, error)
|
||||
openURL func(string) error
|
||||
getEnv func(key string) string
|
||||
listen func(string, string) (net.Listener, error)
|
||||
isTTY func(int) bool
|
||||
getProvider func(*oauth2.Config, *oidc.Provider, *http.Client) provider.UpstreamOIDCIdentityProviderI
|
||||
validateIDToken func(ctx context.Context, provider *oidc.Provider, audience string, token string) (*oidc.IDToken, error)
|
||||
promptForValue func(promptLabel string) (string, error)
|
||||
promptForValue func(ctx context.Context, promptLabel string) (string, error)
|
||||
promptForSecret func(promptLabel string) (string, error)
|
||||
|
||||
callbacks chan callbackResult
|
||||
@@ -140,7 +154,7 @@ func WithLogger(logger logr.Logger) Option {
|
||||
// system at the time of the request.
|
||||
func WithListenPort(port uint16) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.listenAddr = fmt.Sprintf("localhost:%d", port)
|
||||
h.listenAddr = net.JoinHostPort("localhost", fmt.Sprint(port))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -156,6 +170,9 @@ func WithScopes(scopes []string) Option {
|
||||
|
||||
// WithBrowserOpen overrides the default "open browser" functionality with a custom callback. If not specified,
|
||||
// an implementation using https://github.com/pkg/browser will be used by default.
|
||||
//
|
||||
// Deprecated: this option will be removed in a future version of Pinniped. See the
|
||||
// WithSkipBrowserOpen() option instead.
|
||||
func WithBrowserOpen(openURL func(url string) error) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.openURL = openURL
|
||||
@@ -163,6 +180,23 @@ func WithBrowserOpen(openURL func(url string) error) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipBrowserOpen causes the login to only print the authorize URL, but skips attempting to
|
||||
// open the user's default web browser.
|
||||
func WithSkipBrowserOpen() Option {
|
||||
return func(h *handlerState) error {
|
||||
h.openURL = func(_ string) error { return nil }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipListen causes the login skip starting the localhost listener, forcing the manual copy/paste login flow.
|
||||
func WithSkipListen() Option {
|
||||
return func(h *handlerState) error {
|
||||
h.listen = func(string, string) (net.Listener, error) { return nil, nil }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SessionCacheKey contains the data used to select a valid session cache entry.
|
||||
type SessionCacheKey struct {
|
||||
Issuer string `json:"issuer"`
|
||||
@@ -242,7 +276,7 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
||||
callbackPath: "/callback",
|
||||
ctx: context.Background(),
|
||||
logger: logr.Discard(), // discard logs unless a logger is specified
|
||||
callbacks: make(chan callbackResult),
|
||||
callbacks: make(chan callbackResult, 2),
|
||||
httpClient: http.DefaultClient,
|
||||
|
||||
// Default implementations of external dependencies (to be mocked in tests).
|
||||
@@ -250,6 +284,9 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
||||
generateNonce: nonce.Generate,
|
||||
generatePKCE: pkce.Generate,
|
||||
openURL: browser.OpenURL,
|
||||
getEnv: os.Getenv,
|
||||
listen: net.Listen,
|
||||
isTTY: term.IsTerminal,
|
||||
getProvider: upstreamoidc.New,
|
||||
validateIDToken: func(ctx context.Context, provider *oidc.Provider, audience string, token string) (*oidc.IDToken, error) {
|
||||
return provider.Verifier(&oidc.Config{ClientID: audience}).Verify(ctx, token)
|
||||
@@ -375,14 +412,10 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) {
|
||||
// Make a direct call to the authorize endpoint, including the user's username and password on custom http headers,
|
||||
// and parse the authcode from the response. Exchange the authcode for tokens. Return the tokens or an error.
|
||||
func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) {
|
||||
// Ask the user for their username and password.
|
||||
username, err := h.promptForValue(defaultLDAPUsernamePrompt)
|
||||
// Ask the user for their username and password, or get them from env vars.
|
||||
username, password, err := h.getUsernameAndPassword()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error prompting for username: %w", err)
|
||||
}
|
||||
password, err := h.promptForSecret(defaultLDAPPasswordPrompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error prompting for password: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make a callback URL even though we won't be listening on this port, because providing a redirect URL is
|
||||
@@ -472,33 +505,87 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Prompt for the user's username and password, or read them from env vars if they are available.
|
||||
func (h *handlerState) getUsernameAndPassword() (string, string, error) {
|
||||
var err error
|
||||
|
||||
username := h.getEnv(defaultUsernameEnvVarName)
|
||||
if username == "" {
|
||||
username, err = h.promptForValue(h.ctx, defaultLDAPUsernamePrompt)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error prompting for username: %w", err)
|
||||
}
|
||||
} else {
|
||||
h.logger.V(debugLogLevel).Info("Pinniped: Read username from environment variable", "name", defaultUsernameEnvVarName)
|
||||
}
|
||||
|
||||
password := h.getEnv(defaultPasswordEnvVarName)
|
||||
if password == "" {
|
||||
password, err = h.promptForSecret(defaultLDAPPasswordPrompt)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error prompting for password: %w", err)
|
||||
}
|
||||
} else {
|
||||
h.logger.V(debugLogLevel).Info("Pinniped: Read password from environment variable", "name", defaultPasswordEnvVarName)
|
||||
}
|
||||
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
// Open a web browser, or ask the user to open a web browser, to visit the authorize endpoint.
|
||||
// Create a localhost callback listener which exchanges the authcode for tokens. Return the tokens or an error.
|
||||
func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) {
|
||||
// Open a TCP listener and update the OAuth2 redirect_uri to match (in case we are using an ephemeral port number).
|
||||
listener, err := net.Listen("tcp", h.listenAddr)
|
||||
// Attempt to open a local TCP listener, logging but otherwise ignoring any error.
|
||||
listener, err := h.listen("tcp", h.listenAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open callback listener: %w", err)
|
||||
h.logger.V(debugLogLevel).Error(err, "could not open callback listener")
|
||||
}
|
||||
|
||||
// If the listener failed to start and stdin is not a TTY, then we have no hope of succeeding,
|
||||
// since we won't be able to receive the web callback and we can't prompt for the manual auth code.
|
||||
if listener == nil && !h.isTTY(stdin()) {
|
||||
return nil, fmt.Errorf("login failed: must have either a localhost listener or stdin must be a TTY")
|
||||
}
|
||||
|
||||
// Update the OAuth2 redirect_uri to match the actual listener address (if there is one), or just use
|
||||
// a fake ":0" port if there is no listener running.
|
||||
redirectURI := url.URL{Scheme: "http", Path: h.callbackPath}
|
||||
if listener == nil {
|
||||
redirectURI.Host = "127.0.0.1:0"
|
||||
} else {
|
||||
redirectURI.Host = listener.Addr().String()
|
||||
}
|
||||
h.oauth2Config.RedirectURL = redirectURI.String()
|
||||
|
||||
// If the server supports it, request response_mode=form_post.
|
||||
authParams := *authorizeOptions
|
||||
if h.useFormPost {
|
||||
authParams = append(authParams, oauth2.SetAuthURLParam("response_mode", "form_post"))
|
||||
}
|
||||
h.oauth2Config.RedirectURL = (&url.URL{
|
||||
Scheme: "http",
|
||||
Host: listener.Addr().String(),
|
||||
Path: h.callbackPath,
|
||||
}).String()
|
||||
|
||||
// Now that we have a redirect URL with the listener port, we can build the authorize URL.
|
||||
authorizeURL := h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...)
|
||||
authorizeURL := h.oauth2Config.AuthCodeURL(h.state.String(), authParams...)
|
||||
|
||||
// Start a callback server in a background goroutine.
|
||||
shutdown := h.serve(listener)
|
||||
defer shutdown()
|
||||
|
||||
// Open the authorize URL in the users browser.
|
||||
if err := h.openURL(authorizeURL); err != nil {
|
||||
return nil, fmt.Errorf("could not open browser: %w", err)
|
||||
// If there is a listener running, start serving the callback handler in a background goroutine.
|
||||
if listener != nil {
|
||||
shutdown := h.serve(listener)
|
||||
defer shutdown()
|
||||
}
|
||||
|
||||
// Wait for either the callback or a timeout.
|
||||
// Open the authorize URL in the users browser, logging but otherwise ignoring any error.
|
||||
if err := h.openURL(authorizeURL); err != nil {
|
||||
h.logger.V(debugLogLevel).Error(err, "could not open browser")
|
||||
}
|
||||
|
||||
// Prompt the user to visit the authorize URL, and to paste a manually-copied auth code (if possible).
|
||||
ctx, cancel := context.WithCancel(h.ctx)
|
||||
cleanupPrompt := h.promptForWebLogin(ctx, authorizeURL, os.Stderr)
|
||||
defer func() {
|
||||
cancel()
|
||||
cleanupPrompt()
|
||||
}()
|
||||
|
||||
// Wait for either the web callback, a pasted auth code, or a timeout.
|
||||
select {
|
||||
case <-h.ctx.Done():
|
||||
return nil, fmt.Errorf("timed out waiting for token callback: %w", h.ctx.Err())
|
||||
@@ -510,31 +597,90 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp
|
||||
}
|
||||
}
|
||||
|
||||
func promptForValue(promptLabel string) (string, error) {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
func (h *handlerState) promptForWebLogin(ctx context.Context, authorizeURL string, out io.Writer) func() {
|
||||
_, _ = fmt.Fprintf(out, "Log in by visiting this link:\n\n %s\n\n", authorizeURL)
|
||||
|
||||
// If stdin is not a TTY, print the URL but don't prompt for the manual paste,
|
||||
// since we have no way of reading it.
|
||||
if !h.isTTY(stdin()) {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// If the server didn't support response_mode=form_post, don't bother prompting for the manual
|
||||
// code because the user isn't going to have any easy way to manually copy it anyway.
|
||||
if !h.useFormPost {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// Launch the manual auth code prompt in a background goroutine, which will be cancelled
|
||||
// if the parent context is cancelled (when the login succeeds or times out).
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer func() {
|
||||
// Always emit a newline so the kubectl output is visually separated from the login prompts.
|
||||
_, _ = fmt.Fprintln(os.Stderr)
|
||||
|
||||
wg.Done()
|
||||
}()
|
||||
code, err := h.promptForValue(ctx, " Optionally, paste your authorization code: ")
|
||||
if err != nil {
|
||||
// Print a visual marker to show the the prompt is no longer waiting for user input, plus a trailing
|
||||
// newline that simulates the user having pressed "enter".
|
||||
_, _ = fmt.Fprint(os.Stderr, "[...]\n")
|
||||
|
||||
h.callbacks <- callbackResult{err: fmt.Errorf("failed to prompt for manual authorization code: %v", err)}
|
||||
return
|
||||
}
|
||||
|
||||
// When a code is pasted, redeem it for a token and return that result on the callbacks channel.
|
||||
token, err := h.redeemAuthCode(ctx, code)
|
||||
h.callbacks <- callbackResult{token: token, err: err}
|
||||
}()
|
||||
return wg.Wait
|
||||
}
|
||||
|
||||
func promptForValue(ctx context.Context, promptLabel string) (string, error) {
|
||||
if !term.IsTerminal(stdin()) {
|
||||
return "", errors.New("stdin is not connected to a terminal")
|
||||
}
|
||||
_, err := fmt.Fprint(os.Stderr, promptLabel)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not print prompt to stderr: %w", err)
|
||||
}
|
||||
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could read input from stdin: %w", err)
|
||||
|
||||
type readResult struct {
|
||||
text string
|
||||
err error
|
||||
}
|
||||
readResults := make(chan readResult)
|
||||
go func() {
|
||||
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
readResults <- readResult{text, err}
|
||||
close(readResults)
|
||||
}()
|
||||
|
||||
// If the context is canceled, return immediately. The ReadString() operation will stay hung in the background
|
||||
// goroutine indefinitely.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case r := <-readResults:
|
||||
return strings.TrimSpace(r.text), r.err
|
||||
}
|
||||
text = strings.TrimSpace(text)
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func promptForSecret(promptLabel string) (string, error) {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
if !term.IsTerminal(stdin()) {
|
||||
return "", errors.New("stdin is not connected to a terminal")
|
||||
}
|
||||
_, err := fmt.Fprint(os.Stderr, promptLabel)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not print prompt to stderr: %w", err)
|
||||
}
|
||||
password, err := term.ReadPassword(0)
|
||||
password, err := term.ReadPassword(stdin())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read password: %w", err)
|
||||
}
|
||||
@@ -567,9 +713,27 @@ func (h *handlerState) initOIDCDiscovery() error {
|
||||
Endpoint: h.provider.Endpoint(),
|
||||
Scopes: h.scopes,
|
||||
}
|
||||
|
||||
// Use response_mode=form_post if the provider supports it.
|
||||
var discoveryClaims struct {
|
||||
ResponseModesSupported []string `json:"response_modes_supported"`
|
||||
}
|
||||
if err := h.provider.Claims(&discoveryClaims); err != nil {
|
||||
return fmt.Errorf("could not decode response_modes_supported in OIDC discovery from %q: %w", h.issuer, err)
|
||||
}
|
||||
h.useFormPost = stringSliceContains(discoveryClaims.ResponseModesSupported, "form_post")
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringSliceContains(slice []string, s string) bool {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidctypes.Token, error) {
|
||||
h.logger.V(debugLogLevel).Info("Pinniped: Performing RFC8693 token exchange", "requestedAudience", h.requestedAudience)
|
||||
// Perform OIDC discovery. This may have already been performed if there was not a cached base token.
|
||||
@@ -664,13 +828,29 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}()
|
||||
|
||||
// Return HTTP 405 for anything that's not a GET.
|
||||
if r.Method != http.MethodGet {
|
||||
return httperr.Newf(http.StatusMethodNotAllowed, "wanted GET")
|
||||
var params url.Values
|
||||
if h.useFormPost {
|
||||
// Return HTTP 405 for anything that's not a POST.
|
||||
if r.Method != http.MethodPost {
|
||||
return httperr.Newf(http.StatusMethodNotAllowed, "wanted POST")
|
||||
}
|
||||
|
||||
// Parse and pull the response parameters from a application/x-www-form-urlencoded request body.
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return httperr.Wrap(http.StatusBadRequest, "invalid form", err)
|
||||
}
|
||||
params = r.Form
|
||||
} else {
|
||||
// Return HTTP 405 for anything that's not a GET.
|
||||
if r.Method != http.MethodGet {
|
||||
return httperr.Newf(http.StatusMethodNotAllowed, "wanted GET")
|
||||
}
|
||||
|
||||
// Pull response parameters from the URL query string.
|
||||
params = r.URL.Query()
|
||||
}
|
||||
|
||||
// Validate OAuth2 state and fail if it's incorrect (to block CSRF).
|
||||
params := r.URL.Query()
|
||||
if err := h.state.Validate(params.Get("state")); err != nil {
|
||||
return httperr.New(http.StatusForbidden, "missing or invalid state parameter")
|
||||
}
|
||||
@@ -685,14 +865,7 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
|
||||
|
||||
// Exchange the authorization code for access, ID, and refresh tokens and perform required
|
||||
// validations on the returned ID token.
|
||||
token, err := h.getProvider(h.oauth2Config, h.provider, h.httpClient).
|
||||
ExchangeAuthcodeAndValidateTokens(
|
||||
r.Context(),
|
||||
params.Get("code"),
|
||||
h.pkce,
|
||||
h.nonce,
|
||||
h.oauth2Config.RedirectURL,
|
||||
)
|
||||
token, err := h.redeemAuthCode(r.Context(), params.Get("code"))
|
||||
if err != nil {
|
||||
return httperr.Wrap(http.StatusBadRequest, "could not complete code exchange", err)
|
||||
}
|
||||
@@ -702,6 +875,17 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handlerState) redeemAuthCode(ctx context.Context, code string) (*oidctypes.Token, error) {
|
||||
return h.getProvider(h.oauth2Config, h.provider, h.httpClient).
|
||||
ExchangeAuthcodeAndValidateTokens(
|
||||
ctx,
|
||||
code,
|
||||
h.pkce,
|
||||
h.nonce,
|
||||
h.oauth2Config.RedirectURL,
|
||||
)
|
||||
}
|
||||
|
||||
func (h *handlerState) serve(listener net.Listener) func() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(h.callbackPath, httperr.HandlerFunc(h.handleAuthCodeCallback))
|
||||
@@ -711,9 +895,9 @@ func (h *handlerState) serve(listener net.Listener) func() {
|
||||
}
|
||||
go func() { _ = srv.Serve(listener) }()
|
||||
return func() {
|
||||
// Gracefully shut down the server, allowing up to 5 seconds for
|
||||
// Gracefully shut down the server, allowing up to 100ms for
|
||||
// clients to receive any in-flight responses.
|
||||
shutdownCtx, cancel := context.WithTimeout(h.ctx, 5*time.Second)
|
||||
shutdownCtx, cancel := context.WithTimeout(h.ctx, 100*time.Millisecond)
|
||||
_ = srv.Shutdown(shutdownCtx)
|
||||
cancel()
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
package oidcclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -80,6 +83,22 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
}))
|
||||
t.Cleanup(errorServer.Close)
|
||||
|
||||
// Start a test server that returns discovery data with a broken response_modes_supported value.
|
||||
brokenResponseModeMux := http.NewServeMux()
|
||||
brokenResponseModeServer := httptest.NewServer(brokenResponseModeMux)
|
||||
brokenResponseModeMux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("content-type", "application/json")
|
||||
type providerJSON struct {
|
||||
Issuer string `json:"issuer"`
|
||||
ResponseModesSupported string `json:"response_modes_supported"` // Wrong type (should be []string).
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||
Issuer: brokenResponseModeServer.URL,
|
||||
ResponseModesSupported: "invalid",
|
||||
})
|
||||
})
|
||||
t.Cleanup(brokenResponseModeServer.Close)
|
||||
|
||||
// Start a test server that returns discovery data with a broken token URL
|
||||
brokenTokenURLMux := http.NewServeMux()
|
||||
brokenTokenURLServer := httptest.NewServer(brokenTokenURLMux)
|
||||
@@ -100,30 +119,29 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
})
|
||||
t.Cleanup(brokenTokenURLServer.Close)
|
||||
|
||||
// Start a test server that returns a real discovery document and answers refresh requests.
|
||||
providerMux := http.NewServeMux()
|
||||
successServer := httptest.NewServer(providerMux)
|
||||
t.Cleanup(successServer.Close)
|
||||
providerMux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
discoveryHandler := func(server *httptest.Server, responseModes []string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("content-type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(&struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthURL string `json:"authorization_endpoint"`
|
||||
TokenURL string `json:"token_endpoint"`
|
||||
JWKSURL string `json:"jwks_uri"`
|
||||
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
||||
}{
|
||||
Issuer: server.URL,
|
||||
AuthURL: server.URL + "/authorize",
|
||||
TokenURL: server.URL + "/token",
|
||||
JWKSURL: server.URL + "/keys",
|
||||
ResponseModesSupported: responseModes,
|
||||
})
|
||||
}
|
||||
w.Header().Set("content-type", "application/json")
|
||||
type providerJSON struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthURL string `json:"authorization_endpoint"`
|
||||
TokenURL string `json:"token_endpoint"`
|
||||
JWKSURL string `json:"jwks_uri"`
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||
Issuer: successServer.URL,
|
||||
AuthURL: successServer.URL + "/authorize",
|
||||
TokenURL: successServer.URL + "/token",
|
||||
JWKSURL: successServer.URL + "/keys",
|
||||
})
|
||||
})
|
||||
providerMux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
tokenHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
@@ -204,7 +222,21 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
|
||||
w.Header().Set("content-type", "application/json")
|
||||
require.NoError(t, json.NewEncoder(w).Encode(&response))
|
||||
})
|
||||
}
|
||||
|
||||
// Start a test server that returns a real discovery document and answers refresh requests.
|
||||
providerMux := http.NewServeMux()
|
||||
successServer := httptest.NewServer(providerMux)
|
||||
t.Cleanup(successServer.Close)
|
||||
providerMux.HandleFunc("/.well-known/openid-configuration", discoveryHandler(successServer, nil))
|
||||
providerMux.HandleFunc("/token", tokenHandler)
|
||||
|
||||
// Start a test server that returns a real discovery document and answers refresh requests, _and_ supports form_mode=post.
|
||||
formPostProviderMux := http.NewServeMux()
|
||||
formPostSuccessServer := httptest.NewServer(formPostProviderMux)
|
||||
t.Cleanup(formPostSuccessServer.Close)
|
||||
formPostProviderMux.HandleFunc("/.well-known/openid-configuration", discoveryHandler(formPostSuccessServer, []string{"query", "form_post"}))
|
||||
formPostProviderMux.HandleFunc("/token", tokenHandler)
|
||||
|
||||
defaultDiscoveryResponse := func(req *http.Request) (*http.Response, error) { // nolint:unparam
|
||||
// Call the handler function from the test server to calculate the response.
|
||||
@@ -218,8 +250,8 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
h.generateState = func() (state.State, error) { return "test-state", nil }
|
||||
h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil }
|
||||
h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil }
|
||||
h.promptForValue = func(promptLabel string) (string, error) { return "some-upstream-username", nil }
|
||||
h.promptForSecret = func(promptLabel string) (string, error) { return "some-upstream-password", nil }
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) { return "some-upstream-username", nil }
|
||||
h.promptForSecret = func(_ string) (string, error) { return "some-upstream-password", nil }
|
||||
|
||||
cache := &mockSessionCache{t: t, getReturnsToken: nil}
|
||||
cacheKey := SessionCacheKey{
|
||||
@@ -349,7 +381,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
wantToken: &testToken,
|
||||
},
|
||||
{
|
||||
name: "discovery failure",
|
||||
name: "discovery failure due to 500 error",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error { return nil }
|
||||
},
|
||||
@@ -357,6 +389,15 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + errorServer.URL + "\""},
|
||||
wantErr: fmt.Sprintf("could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n", errorServer.URL),
|
||||
},
|
||||
{
|
||||
name: "discovery failure due to invalid response_modes_supported",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error { return nil }
|
||||
},
|
||||
issuer: brokenResponseModeServer.URL,
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + brokenResponseModeServer.URL + "\""},
|
||||
wantErr: fmt.Sprintf("could not decode response_modes_supported in OIDC discovery from %q: json: cannot unmarshal string into Go struct field .response_modes_supported of type []string", brokenResponseModeServer.URL),
|
||||
},
|
||||
{
|
||||
name: "session cache hit with refreshable token",
|
||||
issuer: successServer.URL,
|
||||
@@ -451,38 +492,93 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
})
|
||||
h.cache = cache
|
||||
|
||||
h.listenAddr = "invalid-listen-address"
|
||||
|
||||
h.listen = func(string, string) (net.Listener, error) { return nil, fmt.Errorf("some listen error") }
|
||||
h.isTTY = func(int) bool { return false }
|
||||
return nil
|
||||
}
|
||||
},
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
|
||||
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\""},
|
||||
wantLogs: []string{
|
||||
`"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + successServer.URL + `"`,
|
||||
`"level"=4 "msg"="Pinniped: Refreshing cached token."`,
|
||||
`"msg"="could not open callback listener" "error"="some listen error"`,
|
||||
},
|
||||
// Expect this to fall through to the authorization code flow, so it fails here.
|
||||
wantErr: "could not open callback listener: listen tcp: address invalid-listen-address: missing port in address",
|
||||
wantErr: "login failed: must have either a localhost listener or stdin must be a TTY",
|
||||
},
|
||||
{
|
||||
name: "listen failure",
|
||||
name: "listen failure and non-tty stdin",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.listenAddr = "invalid-listen-address"
|
||||
h.listen = func(net string, addr string) (net.Listener, error) {
|
||||
assert.Equal(t, "tcp", net)
|
||||
assert.Equal(t, "localhost:0", addr)
|
||||
return nil, fmt.Errorf("some listen error")
|
||||
}
|
||||
h.isTTY = func(fd int) bool {
|
||||
assert.Equal(t, fd, syscall.Stdin)
|
||||
return false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
issuer: successServer.URL,
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
|
||||
wantErr: "could not open callback listener: listen tcp: address invalid-listen-address: missing port in address",
|
||||
issuer: successServer.URL,
|
||||
wantLogs: []string{
|
||||
`"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + successServer.URL + `"`,
|
||||
`"msg"="could not open callback listener" "error"="some listen error"`,
|
||||
},
|
||||
wantErr: "login failed: must have either a localhost listener or stdin must be a TTY",
|
||||
},
|
||||
{
|
||||
name: "browser open failure",
|
||||
name: "listening disabled and manual prompt fails",
|
||||
opt: func(t *testing.T) Option {
|
||||
return WithBrowserOpen(func(url string) error {
|
||||
return fmt.Errorf("some browser open error")
|
||||
})
|
||||
return func(h *handlerState) error {
|
||||
require.NoError(t, WithSkipListen()(h))
|
||||
h.isTTY = func(fd int) bool { return true }
|
||||
h.openURL = func(authorizeURL string) error {
|
||||
parsed, err := url.Parse(authorizeURL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "http://127.0.0.1:0/callback", parsed.Query().Get("redirect_uri"))
|
||||
require.Equal(t, "form_post", parsed.Query().Get("response_mode"))
|
||||
return fmt.Errorf("some browser open error")
|
||||
}
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
return "", fmt.Errorf("some prompt error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
issuer: successServer.URL,
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
|
||||
wantErr: "could not open browser: some browser open error",
|
||||
issuer: formPostSuccessServer.URL,
|
||||
wantLogs: []string{
|
||||
`"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + formPostSuccessServer.URL + `"`,
|
||||
`"msg"="could not open browser" "error"="some browser open error"`,
|
||||
},
|
||||
wantErr: "error handling callback: failed to prompt for manual authorization code: some prompt error",
|
||||
},
|
||||
{
|
||||
name: "listen success and manual prompt succeeds",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.listen = func(string, string) (net.Listener, error) { return nil, fmt.Errorf("some listen error") }
|
||||
h.isTTY = func(fd int) bool { return true }
|
||||
h.openURL = func(authorizeURL string) error {
|
||||
parsed, err := url.Parse(authorizeURL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "http://127.0.0.1:0/callback", parsed.Query().Get("redirect_uri"))
|
||||
require.Equal(t, "form_post", parsed.Query().Get("response_mode"))
|
||||
return nil
|
||||
}
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
return "", fmt.Errorf("some prompt error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
issuer: formPostSuccessServer.URL,
|
||||
wantLogs: []string{
|
||||
`"level"=4 "msg"="Pinniped: Performing OIDC discovery" "issuer"="` + formPostSuccessServer.URL + `"`,
|
||||
`"msg"="could not open callback listener" "error"="some listen error"`,
|
||||
},
|
||||
wantErr: "error handling callback: failed to prompt for manual authorization code: some prompt error",
|
||||
},
|
||||
{
|
||||
name: "timeout waiting for callback",
|
||||
@@ -580,6 +676,68 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
|
||||
wantToken: &testToken,
|
||||
},
|
||||
{
|
||||
name: "callback returns success with request_mode=form_post",
|
||||
clientID: "test-client-id",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.generateState = func() (state.State, error) { return "test-state", nil }
|
||||
h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil }
|
||||
h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil }
|
||||
|
||||
cache := &mockSessionCache{t: t, getReturnsToken: nil}
|
||||
cacheKey := SessionCacheKey{
|
||||
Issuer: formPostSuccessServer.URL,
|
||||
ClientID: "test-client-id",
|
||||
Scopes: []string{"test-scope"},
|
||||
RedirectURI: "http://localhost:0/callback",
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys)
|
||||
require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawPutKeys)
|
||||
require.Equal(t, []*oidctypes.Token{&testToken}, cache.sawPutTokens)
|
||||
})
|
||||
require.NoError(t, WithSessionCache(cache)(h))
|
||||
require.NoError(t, WithClient(&http.Client{Timeout: 10 * time.Second})(h))
|
||||
|
||||
h.openURL = func(actualURL string) error {
|
||||
parsedActualURL, err := url.Parse(actualURL)
|
||||
require.NoError(t, err)
|
||||
actualParams := parsedActualURL.Query()
|
||||
|
||||
require.Contains(t, actualParams.Get("redirect_uri"), "http://127.0.0.1:")
|
||||
actualParams.Del("redirect_uri")
|
||||
|
||||
require.Equal(t, url.Values{
|
||||
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
|
||||
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
|
||||
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
|
||||
"code_challenge": []string{"VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"},
|
||||
"code_challenge_method": []string{"S256"},
|
||||
"response_type": []string{"code"},
|
||||
"response_mode": []string{"form_post"},
|
||||
"scope": []string{"test-scope"},
|
||||
"nonce": []string{"test-nonce"},
|
||||
"state": []string{"test-state"},
|
||||
"access_type": []string{"offline"},
|
||||
"client_id": []string{"test-client-id"},
|
||||
}, actualParams)
|
||||
|
||||
parsedActualURL.RawQuery = ""
|
||||
require.Equal(t, formPostSuccessServer.URL+"/authorize", parsedActualURL.String())
|
||||
|
||||
go func() {
|
||||
h.callbacks <- callbackResult{token: &testToken}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
issuer: formPostSuccessServer.URL,
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + formPostSuccessServer.URL + "\""},
|
||||
wantToken: &testToken,
|
||||
},
|
||||
{
|
||||
name: "upstream name and type are included in authorize request if upstream name is provided",
|
||||
clientID: "test-client-id",
|
||||
@@ -650,7 +808,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
_ = defaultLDAPTestOpts(t, h, nil, nil)
|
||||
h.promptForValue = func(promptLabel string) (string, error) {
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
require.Equal(t, "Username: ", promptLabel)
|
||||
return "", errors.New("some prompt error")
|
||||
}
|
||||
@@ -667,7 +825,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
_ = defaultLDAPTestOpts(t, h, nil, nil)
|
||||
h.promptForSecret = func(promptLabel string) (string, error) { return "", errors.New("some prompt error") }
|
||||
h.promptForSecret = func(_ string) (string, error) { return "", errors.New("some prompt error") }
|
||||
return nil
|
||||
}
|
||||
},
|
||||
@@ -835,7 +993,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
wantErr: "error during authorization code exchange: some authcode exchange or token validation error",
|
||||
},
|
||||
{
|
||||
name: "successful ldap login",
|
||||
name: "successful ldap login with prompts for username and password",
|
||||
clientID: "test-client-id",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
@@ -853,7 +1011,10 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
h.generateState = func() (state.State, error) { return "test-state", nil }
|
||||
h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil }
|
||||
h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil }
|
||||
h.promptForValue = func(promptLabel string) (string, error) {
|
||||
h.getEnv = func(_ string) string {
|
||||
return "" // asking for any env var returns empty as if it were unset
|
||||
}
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
require.Equal(t, "Username: ", promptLabel)
|
||||
return "some-upstream-username", nil
|
||||
}
|
||||
@@ -931,6 +1092,117 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
|
||||
wantToken: &testToken,
|
||||
},
|
||||
{
|
||||
name: "successful ldap login with env vars for username and password",
|
||||
clientID: "test-client-id",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
fakeAuthCode := "test-authcode-value"
|
||||
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ExchangeAuthcodeAndValidateTokens(
|
||||
gomock.Any(), fakeAuthCode, pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), "http://127.0.0.1:0/callback").
|
||||
Return(&testToken, nil)
|
||||
return mock
|
||||
}
|
||||
|
||||
h.generateState = func() (state.State, error) { return "test-state", nil }
|
||||
h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil }
|
||||
h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil }
|
||||
h.getEnv = func(key string) string {
|
||||
switch key {
|
||||
case "PINNIPED_USERNAME":
|
||||
return "some-upstream-username"
|
||||
case "PINNIPED_PASSWORD":
|
||||
return "some-upstream-password"
|
||||
default:
|
||||
return "" // all other env vars are treated as if they are unset
|
||||
}
|
||||
}
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
require.FailNow(t, fmt.Sprintf("saw unexpected prompt from the CLI: %q", promptLabel))
|
||||
return "", nil
|
||||
}
|
||||
h.promptForSecret = func(promptLabel string) (string, error) {
|
||||
require.FailNow(t, fmt.Sprintf("saw unexpected prompt from the CLI: %q", promptLabel))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
cache := &mockSessionCache{t: t, getReturnsToken: nil}
|
||||
cacheKey := SessionCacheKey{
|
||||
Issuer: successServer.URL,
|
||||
ClientID: "test-client-id",
|
||||
Scopes: []string{"test-scope"},
|
||||
RedirectURI: "http://localhost:0/callback",
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys)
|
||||
require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawPutKeys)
|
||||
require.Equal(t, []*oidctypes.Token{&testToken}, cache.sawPutTokens)
|
||||
})
|
||||
require.NoError(t, WithSessionCache(cache)(h))
|
||||
require.NoError(t, WithCLISendingCredentials()(h))
|
||||
require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "ldap")(h))
|
||||
|
||||
discoveryRequestWasMade := false
|
||||
authorizeRequestWasMade := false
|
||||
t.Cleanup(func() {
|
||||
require.True(t, discoveryRequestWasMade, "should have made an discovery request")
|
||||
require.True(t, authorizeRequestWasMade, "should have made an authorize request")
|
||||
})
|
||||
|
||||
require.NoError(t, WithClient(&http.Client{
|
||||
Transport: roundtripper.Func(func(req *http.Request) (*http.Response, error) {
|
||||
switch req.URL.Scheme + "://" + req.URL.Host + req.URL.Path {
|
||||
case "http://" + successServer.Listener.Addr().String() + "/.well-known/openid-configuration":
|
||||
discoveryRequestWasMade = true
|
||||
return defaultDiscoveryResponse(req)
|
||||
case "http://" + successServer.Listener.Addr().String() + "/authorize":
|
||||
authorizeRequestWasMade = true
|
||||
require.Equal(t, "some-upstream-username", req.Header.Get("Pinniped-Username"))
|
||||
require.Equal(t, "some-upstream-password", req.Header.Get("Pinniped-Password"))
|
||||
require.Equal(t, url.Values{
|
||||
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
|
||||
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
|
||||
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
|
||||
"code_challenge": []string{"VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"},
|
||||
"code_challenge_method": []string{"S256"},
|
||||
"response_type": []string{"code"},
|
||||
"scope": []string{"test-scope"},
|
||||
"nonce": []string{"test-nonce"},
|
||||
"state": []string{"test-state"},
|
||||
"access_type": []string{"offline"},
|
||||
"client_id": []string{"test-client-id"},
|
||||
"redirect_uri": []string{"http://127.0.0.1:0/callback"},
|
||||
"pinniped_idp_name": []string{"some-upstream-name"},
|
||||
"pinniped_idp_type": []string{"ldap"},
|
||||
}, req.URL.Query())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusFound,
|
||||
Header: http.Header{"Location": []string{
|
||||
fmt.Sprintf("http://127.0.0.1:0/callback?code=%s&state=test-state", fakeAuthCode),
|
||||
}},
|
||||
}, nil
|
||||
default:
|
||||
// Note that "/token" requests should not be made. They are mocked by mocking calls to ExchangeAuthcodeAndValidateTokens().
|
||||
require.FailNow(t, fmt.Sprintf("saw unexpected http call from the CLI: %s", req.URL.String()))
|
||||
return nil, nil
|
||||
}
|
||||
}),
|
||||
})(h))
|
||||
return nil
|
||||
}
|
||||
},
|
||||
issuer: successServer.URL,
|
||||
wantLogs: []string{
|
||||
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
|
||||
"\"level\"=4 \"msg\"=\"Pinniped: Read username from environment variable\" \"name\"=\"PINNIPED_USERNAME\"",
|
||||
"\"level\"=4 \"msg\"=\"Pinniped: Read password from environment variable\" \"name\"=\"PINNIPED_PASSWORD\"",
|
||||
},
|
||||
wantToken: &testToken,
|
||||
},
|
||||
{
|
||||
name: "with requested audience, session cache hit with valid token, but discovery fails",
|
||||
clientID: "test-client-id",
|
||||
@@ -1287,10 +1559,11 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
WithContext(context.Background()),
|
||||
WithListenPort(0),
|
||||
WithScopes([]string{"test-scope"}),
|
||||
WithSkipBrowserOpen(),
|
||||
tt.opt(t),
|
||||
WithLogger(testLogger),
|
||||
)
|
||||
require.Equal(t, tt.wantLogs, testLogger.Lines())
|
||||
testLogger.Expect(tt.wantLogs)
|
||||
if tt.wantErr != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
require.Nil(t, tok)
|
||||
@@ -1324,13 +1597,152 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePasteCallback(t *testing.T) {
|
||||
const testRedirectURI = "http://127.0.0.1:12324/callback"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opt func(t *testing.T) Option
|
||||
wantCallback *callbackResult
|
||||
}{
|
||||
{
|
||||
name: "no stdin available",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.isTTY = func(fd int) bool {
|
||||
require.Equal(t, syscall.Stdin, fd)
|
||||
return false
|
||||
}
|
||||
h.useFormPost = true
|
||||
return nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no form_post mode available",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.isTTY = func(fd int) bool { return true }
|
||||
h.useFormPost = false
|
||||
return nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompt fails",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.isTTY = func(fd int) bool { return true }
|
||||
h.useFormPost = true
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
assert.Equal(t, " Optionally, paste your authorization code: ", promptLabel)
|
||||
return "", fmt.Errorf("some prompt error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
wantCallback: &callbackResult{
|
||||
err: fmt.Errorf("failed to prompt for manual authorization code: some prompt error"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "redeeming code fails",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.isTTY = func(fd int) bool { return true }
|
||||
h.useFormPost = true
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
return "invalid", nil
|
||||
}
|
||||
h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI}
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ExchangeAuthcodeAndValidateTokens(gomock.Any(), "invalid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI).
|
||||
Return(nil, fmt.Errorf("some exchange error"))
|
||||
return mock
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
wantCallback: &callbackResult{
|
||||
err: fmt.Errorf("some exchange error"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.isTTY = func(fd int) bool { return true }
|
||||
h.useFormPost = true
|
||||
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||
return "valid", nil
|
||||
}
|
||||
h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI}
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI).
|
||||
Return(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: "test-id-token"}}, nil)
|
||||
return mock
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
wantCallback: &callbackResult{
|
||||
token: &oidctypes.Token{IDToken: &oidctypes.IDToken{Token: "test-id-token"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &handlerState{
|
||||
callbacks: make(chan callbackResult, 1),
|
||||
state: state.State("test-state"),
|
||||
pkce: pkce.Code("test-pkce"),
|
||||
nonce: nonce.Nonce("test-nonce"),
|
||||
}
|
||||
if tt.opt != nil {
|
||||
require.NoError(t, tt.opt(t)(h))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
h.promptForWebLogin(ctx, "https://test-authorize-url/", &buf)
|
||||
require.Equal(t,
|
||||
"Log in by visiting this link:\n\n https://test-authorize-url/\n\n",
|
||||
buf.String(),
|
||||
)
|
||||
|
||||
if tt.wantCallback != nil {
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
require.Fail(t, "timed out waiting to receive from callbacks channel")
|
||||
case result := <-h.callbacks:
|
||||
require.Equal(t, *tt.wantCallback, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAuthCodeCallback(t *testing.T) {
|
||||
const testRedirectURI = "http://127.0.0.1:12324/callback"
|
||||
|
||||
withFormPostMode := func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.useFormPost = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
query string
|
||||
body []byte
|
||||
contentType string
|
||||
opt func(t *testing.T) Option
|
||||
wantErr string
|
||||
wantHTTPStatus int
|
||||
@@ -1342,6 +1754,24 @@ func TestHandleAuthCodeCallback(t *testing.T) {
|
||||
wantErr: "wanted GET",
|
||||
wantHTTPStatus: http.StatusMethodNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "wrong method for form_post",
|
||||
method: "GET",
|
||||
query: "",
|
||||
opt: withFormPostMode,
|
||||
wantErr: "wanted POST",
|
||||
wantHTTPStatus: http.StatusMethodNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "invalid form for form_post",
|
||||
method: "POST",
|
||||
query: "",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: []byte(`%`),
|
||||
opt: withFormPostMode,
|
||||
wantErr: `invalid form: invalid URL escape "%"`,
|
||||
wantHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid state",
|
||||
query: "state=invalid",
|
||||
@@ -1396,6 +1826,26 @@ func TestHandleAuthCodeCallback(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid form_post",
|
||||
method: http.MethodPost,
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: []byte(`state=test-state&code=valid`),
|
||||
opt: func(t *testing.T) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.useFormPost = true
|
||||
h.oauth2Config = &oauth2.Config{RedirectURL: testRedirectURI}
|
||||
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||
mock := mockUpstream(t)
|
||||
mock.EXPECT().
|
||||
ExchangeAuthcodeAndValidateTokens(gomock.Any(), "valid", pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), testRedirectURI).
|
||||
Return(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: "test-id-token"}}, nil)
|
||||
return mock
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@@ -1414,12 +1864,15 @@ func TestHandleAuthCodeCallback(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "/test-callback", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "/test-callback", bytes.NewBuffer(tt.body))
|
||||
require.NoError(t, err)
|
||||
req.URL.RawQuery = tt.query
|
||||
if tt.method != "" {
|
||||
req.Method = tt.method
|
||||
}
|
||||
if tt.contentType != "" {
|
||||
req.Header.Set("Content-Type", tt.contentType)
|
||||
}
|
||||
|
||||
err = h.handleAuthCodeCallback(resp, req)
|
||||
if tt.wantErr != "" {
|
||||
|
||||
@@ -7,6 +7,7 @@ params:
|
||||
github_url: "https://github.com/vmware-tanzu/pinniped"
|
||||
slack_url: "https://kubernetes.slack.com/messages/pinniped"
|
||||
community_url: "https://go.pinniped.dev/community"
|
||||
latest_version: v0.9.2
|
||||
pygmentsCodefences: true
|
||||
pygmentsStyle: "pygments"
|
||||
markup:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Pinniped How-To Guides
|
||||
title: Pinniped how-to guides
|
||||
cascade:
|
||||
layout: docs
|
||||
menu:
|
||||
|
||||
@@ -15,6 +15,7 @@ This guide shows you how to use this capability _without_ the Pinniped Superviso
|
||||
This is most useful if you have only a single cluster and want to authenticate to it via an existing OIDC provider.
|
||||
|
||||
If you have multiple clusters, you may want to [install]({{< ref "install-supervisor" >}}) and [configure]({{< ref "configure-supervisor" >}}) the Pinniped Supervisor.
|
||||
Then you can [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -121,7 +122,7 @@ You should see:
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding my-user-admin \
|
||||
--clusterrole admin \
|
||||
--clusterrole edit \
|
||||
--user my-username@example.com
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Configure the Pinniped Concierge to validate JWT tokens issued by the Pinniped Supervisor
|
||||
description: Set up JSON Web Token (JWT) based token authentication on an individual Kubernetes cluster using the Pinniped Supervisor as the OIDC Provider.
|
||||
description: Set up JSON Web Token (JWT) based token authentication on an individual Kubernetes cluster using the Pinniped Supervisor as the OIDC provider.
|
||||
cascade:
|
||||
layout: docs
|
||||
menu:
|
||||
@@ -26,6 +26,9 @@ If you would rather not use the Supervisor, you may want to [configure the Conci
|
||||
This how-to guide assumes that you have already [installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress,
|
||||
and that you have [configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}).
|
||||
|
||||
It also assumes that you have configured an `OIDCIdentityProvider` or an `LDAPIdentityProvider` for the Supervisor as the source of your user's identities.
|
||||
Various examples of configuring these resources can be found in these guides.
|
||||
|
||||
It also assumes that you have already [installed the Pinniped Concierge]({{< ref "install-concierge" >}})
|
||||
on all the clusters in which you would like to allow users to have a unified identity.
|
||||
|
||||
@@ -64,62 +67,6 @@ kubectl apply -f my-supervisor-authenticator.yaml
|
||||
Do this on each cluster in which you would like to allow users from that FederationDomain to log in.
|
||||
Don't forget to give each cluster a unique `audience` value for security reasons.
|
||||
|
||||
## Generate a kubeconfig file
|
||||
## Next steps
|
||||
|
||||
Generate a kubeconfig file for one of the clusters in which you installed and configured the Concierge as described above:
|
||||
|
||||
```sh
|
||||
pinniped get kubeconfig > my-cluster.yaml
|
||||
```
|
||||
|
||||
This assumes that your current kubeconfig is an admin-level kubeconfig for the cluster, such as the kubeconfig
|
||||
that you used to install the Concierge.
|
||||
|
||||
This creates a kubeconfig YAML file `my-cluster.yaml`, unique to that cluster, which targets your JWTAuthenticator
|
||||
using `pinniped login oidc` as an [ExecCredential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
This new kubeconfig can be shared with the other users of this cluster. It does not contain any specific
|
||||
identity or credentials. When a user uses this new kubeconfig with `kubectl`, the Pinniped plugin will
|
||||
prompt them to log in using their own identity.
|
||||
|
||||
## Use the kubeconfig file
|
||||
|
||||
Use the kubeconfig with `kubectl` to access your cluster:
|
||||
|
||||
```sh
|
||||
kubectl --kubeconfig my-cluster.yaml get namespaces
|
||||
```
|
||||
|
||||
You should see:
|
||||
|
||||
- The `pinniped login oidc` command is executed automatically by `kubectl`.
|
||||
|
||||
- Pinniped directs you to login with whatever identity provider is configured in the Supervisor, either by opening
|
||||
your browser (for upstream OIDC Providers) or by prompting for your username and password (for upstream LDAP providers).
|
||||
|
||||
- In your shell, you see your clusters namespaces.
|
||||
|
||||
If instead you get an access denied error, you may need to create a ClusterRoleBinding for username of your account
|
||||
in the Supervisor's upstream identity provider, for example:
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding my-user-admin \
|
||||
--clusterrole admin \
|
||||
--user my-username@example.com
|
||||
```
|
||||
|
||||
Alternatively, you could create role bindings based on the group membership of your users
|
||||
in the upstream identity provider, for example:
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding my-auditors \
|
||||
--clusterrole view \
|
||||
--group auditors
|
||||
```
|
||||
|
||||
## Other notes
|
||||
|
||||
- Pinniped kubeconfig files do not contain secrets and are safe to share between users.
|
||||
|
||||
- Temporary session credentials such as ID, access, and refresh tokens are stored in:
|
||||
- `~/.config/pinniped/sessions.yaml` (macOS/Linux)
|
||||
- `%USERPROFILE%/.config/pinniped/sessions.yaml` (Windows).
|
||||
Next, [log in to your cluster]({{< ref "login" >}})!
|
||||
|
||||
@@ -112,5 +112,5 @@ You should see:
|
||||
If instead you get an access denied error, you may need to create a ClusterRoleBinding for the username/groups returned by your webhook, for example:
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding my-user-admin --clusterrole admin --user my-username
|
||||
kubectl create clusterrolebinding my-user-admin --clusterrole edit --user my-username
|
||||
```
|
||||
|
||||
138
site/content/docs/howto/configure-supervisor-with-dex.md
Normal file
138
site/content/docs/howto/configure-supervisor-with-dex.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: Configure the Pinniped Supervisor to use Dex with Github as an OIDC provider
|
||||
description: Set up the Pinniped Supervisor to use Dex login.
|
||||
cascade:
|
||||
layout: docs
|
||||
menu:
|
||||
docs:
|
||||
name: Configure Supervisor With Dex OIDC
|
||||
weight: 80
|
||||
parent: howtos
|
||||
---
|
||||
|
||||
The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting a single
|
||||
"upstream" identity provider to many "downstream" cluster clients.
|
||||
|
||||
This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes
|
||||
cluster using Dex and Github.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This how-to guide assumes that you have already [installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress,
|
||||
and that you have [configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}).
|
||||
|
||||
You'd also have to have an instance of Dex up and running, i.e. accessible at `https://<dex-dns-record>`. You can refer to the [Getting started with Dex](https://dexidp.io/docs/getting-started/) guidelines for more information on how to deploy it.
|
||||
|
||||
## Configure Dex to use Github as an external identity provider
|
||||
|
||||
Dex is an OIDC issuer that supports various identity providers through connectors, i.e. LDAP, Github, Gitlab, Google, SAML and much more. Take a look at its [documentation](https://dexidp.io/docs/connectors/) to understand how to configure such connector in Dex.
|
||||
|
||||
In this example, we'll show how to use Dex to identify users through their GitHub account.
|
||||
|
||||
First, we need to go to your Github account settings and [create an OAuth app](https://github.com/settings/applications/new) by populating the following rows -
|
||||
|
||||
- Application name - `Dex application`
|
||||
- Homepage URL - `https://<dex-dns-record>`
|
||||
- Authorization callback URL - `https://<dex-dns-record>/callback` // this is where Github will redirect you to once your app has authorized
|
||||
|
||||
Once completed, copy your `Client ID` and `Client secret` (generate one if there's none) as those two will be needed to configure a Github connector in Dex.
|
||||
|
||||
To setup one, edit the configuration used by Dex by adding the following -
|
||||
|
||||
```bash
|
||||
...
|
||||
connectors:
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: $GITHUB_CLIENT_ID
|
||||
clientSecret: $GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://<dex-dns-record>/callback
|
||||
...
|
||||
```
|
||||
|
||||
## Register an application in Dex
|
||||
|
||||
Follow the instructions for [registering an application in Dex](https://dexidp.io/docs/using-dex/#configuring-your-app) and create a static client application, in our case the client happens be the Supervisor. Note that the "openid" scope is always included, but you can always request additional scopes that you can then pass to your Kubernetes cluster, such as "groups" for example.
|
||||
|
||||
To create a static client application, edit the configuration used by Dex (can be a file or a ConfigMap) by adding the following -
|
||||
|
||||
```bash
|
||||
...
|
||||
staticClients:
|
||||
- id: pinniped-supervisor
|
||||
secret: pinniped-supervisor-secret
|
||||
name: 'Pinniped Supervisor client'
|
||||
redirectURIs:
|
||||
- 'http://<pinniped-supervisor-dns-record>/callback'
|
||||
...
|
||||
```
|
||||
|
||||
## Configure the Supervisor
|
||||
|
||||
Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/1.20/README.adoc#oidcidentityprovider) resource in the same namespace as the Supervisor.
|
||||
|
||||
For example, the following OIDCIdentityProvider and the corresponding Secret use Dex's `email` claim as the Kubernetes username:
|
||||
|
||||
```yaml
|
||||
apiVersion: idp.supervisor.pinniped.dev/v1alpha1
|
||||
kind: OIDCIdentityProvider
|
||||
metadata:
|
||||
namespace: pinniped-supervisor
|
||||
name: dex
|
||||
spec:
|
||||
# Specify the upstream issuer URL (no trailing slash).
|
||||
issuer: https://<dex-dns-record>
|
||||
|
||||
# Request any scopes other than "openid" for claims besides
|
||||
# the default claims in your token. The "openid" scope is always
|
||||
# included.
|
||||
authorizationConfig:
|
||||
additionalScopes: [groups, email]
|
||||
|
||||
# Specify how Dex claims are mapped to Kubernetes identities.
|
||||
claims:
|
||||
# Specify the name of the claim in your Dex ID token that will be mapped
|
||||
# to the "username" claim in downstream tokens minted by the Supervisor.
|
||||
username: email
|
||||
|
||||
# Specify the name of the claim in your Dex ID token that represents the groups
|
||||
# that the user belongs to. This matches what you specified above
|
||||
# with the Groups claim filter.
|
||||
# Note that the group claims from Github are in the format of "org:team".
|
||||
# To query for the group scope, you should set the organization you want Dex to
|
||||
# search against in its configuration, otherwise your group claim would be empty.
|
||||
# An example config can be found at - https://dexidp.io/docs/connectors/github/#configuration
|
||||
groups: groups
|
||||
|
||||
# Specify the name of the Kubernetes Secret that contains your Dex
|
||||
# application's client credentials (created below).
|
||||
client:
|
||||
secretName: dex-client-credentials
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
namespace: pinniped-supervisor
|
||||
name: dex-client-credentials
|
||||
type: secrets.pinniped.dev/oidc-client
|
||||
stringData:
|
||||
# The "Client ID" that you set in Dex. For example, in our case this is "pinniped-supervisor"
|
||||
clientID: "<your-client-id>"
|
||||
|
||||
# The "Client secret" that you set in Dex. For example, in our case this is "pinniped-supervisor-secret"
|
||||
clientSecret: "<your-client-secret>"
|
||||
```
|
||||
|
||||
Once your OIDCIdentityProvider resource has been created, you can validate your configuration by running:
|
||||
|
||||
```bash
|
||||
kubectl describe OIDCIdentityProvider -n pinniped-supervisor dex
|
||||
```
|
||||
|
||||
Look at the `status` field. If it was configured correctly, you should see `phase: Ready`.
|
||||
|
||||
## Next steps
|
||||
|
||||
Now that you have configured the Supervisor to use Dex, you will want to [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Configure the Pinniped Supervisor to use GitLab as an OIDC Provider
|
||||
title: Configure the Pinniped Supervisor to use GitLab as an OIDC provider
|
||||
description: Set up the Pinniped Supervisor to use GitLab login.
|
||||
cascade:
|
||||
layout: docs
|
||||
@@ -136,6 +136,7 @@ spec:
|
||||
# [...]
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
## Next steps
|
||||
|
||||
Now that you have configured the Supervisor to use GitLab, you will want to [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})!
|
||||
Then you'll be able to log into those clusters as any of the users from the GitLab directory.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Configure the Pinniped Supervisor to use JumpCloud as an LDAP Provider
|
||||
title: Configure the Pinniped Supervisor to use JumpCloud as an LDAP provider
|
||||
description: Set up the Pinniped Supervisor to use JumpCloud LDAP
|
||||
cascade:
|
||||
layout: docs
|
||||
@@ -152,7 +152,7 @@ kubectl describe LDAPIdentityProvider -n pinniped-supervisor jumpcloudldap
|
||||
|
||||
Look at the `status` field. If it was configured correctly, you should see `phase: Ready`.
|
||||
|
||||
## Next Steps
|
||||
## Next steps
|
||||
|
||||
Now that you have configured the Supervisor to use JumpCloud LDAP, you will want to [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})!
|
||||
Then you'll be able to log into those clusters as any of the users from the JumpCloud directory.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Configure the Pinniped Supervisor to use Okta as an OIDC Provider
|
||||
title: Configure the Pinniped Supervisor to use Okta as an OIDC provider
|
||||
description: Set up the Pinniped Supervisor to use Okta login.
|
||||
cascade:
|
||||
layout: docs
|
||||
@@ -108,4 +108,5 @@ Look at the `status` field. If it was configured correctly, you should see `phas
|
||||
|
||||
## Next steps
|
||||
|
||||
Now that you have configured the Supervisor to use Okta, you will want to [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})!
|
||||
Then you'll be able to log into those clusters as any of the users from the Okta directory.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Configure the Pinniped Supervisor to use OpenLDAP as an LDAP Provider
|
||||
title: Configure the Pinniped Supervisor to use OpenLDAP as an LDAP provider
|
||||
description: Set up the Pinniped Supervisor to use OpenLDAP login.
|
||||
cascade:
|
||||
layout: docs
|
||||
@@ -22,7 +22,7 @@ cluster using their identity from an OpenLDAP server.
|
||||
This how-to guide assumes that you have already [installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress,
|
||||
and that you have [configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}).
|
||||
|
||||
## An Example of Deploying OpenLDAP on Kubernetes
|
||||
## An example of deploying OpenLDAP on Kubernetes
|
||||
|
||||
*Note: If you already have an OpenLDAP server installed and configured, please skip to the next section to configure the Supervisor.*
|
||||
|
||||
@@ -292,7 +292,7 @@ kubectl describe LDAPIdentityProvider -n pinniped-supervisor openldap
|
||||
|
||||
Look at the `status` field. If it was configured correctly, you should see `phase: Ready`.
|
||||
|
||||
## Next Steps
|
||||
## Next steps
|
||||
|
||||
Now that you have configured the Supervisor to use OpenLDAP, you will want to [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})!
|
||||
Then you'll be able to log into those clusters as any of the users from the OpenLDAP directory.
|
||||
|
||||
@@ -49,7 +49,7 @@ The most common ways are:
|
||||
and the service.
|
||||
|
||||
For either of the first two options, if you installed using `ytt` then you can use
|
||||
the related `service_*` options from [deploy/supervisor/values.yml](values.yaml) to create a Service.
|
||||
the related `service_*` options from [deploy/supervisor/values.yml](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/supervisor/values.yaml) to create a Service.
|
||||
If you installed using `install-supervisor.yaml` then you can create
|
||||
the Service separately after installing the Supervisor app. There is no `Ingress` included in the `ytt` templates,
|
||||
so if you choose to use an Ingress then you'll need to create that separately after installing the Supervisor app.
|
||||
@@ -163,3 +163,9 @@ You can create the certificate Secrets however you like, for example you could u
|
||||
or `kubectl create secret tls`.
|
||||
Keep in mind that your users must load some of these endpoints in their web browsers, so the TLS certificates
|
||||
should be signed by a certificate authority that is trusted by their browsers.
|
||||
|
||||
## Next steps
|
||||
|
||||
Next, configure an `OIDCIdentityProvider` or an `LDAPIdentityProvider` for the Supervisor (several examples are available in these guides),
|
||||
and [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}})
|
||||
on each cluster!
|
||||
|
||||
@@ -23,11 +23,11 @@ Use [Homebrew](https://brew.sh/) to install from the Pinniped [tap](https://gith
|
||||
|
||||
Find the appropriate binary for your platform from the [latest release](https://github.com/vmware-tanzu/pinniped/releases/latest):
|
||||
|
||||
{{< buttonlink href="https://get.pinniped.dev/latest/pinniped-cli-darwin-amd64" >}}Download for macOS/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}}
|
||||
{{< buttonlink filename="pinniped-cli-darwin-amd64" >}}Download {{< latestversion >}} for macOS/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}}
|
||||
|
||||
{{< buttonlink href="https://get.pinniped.dev/latest/pinniped-cli-linux-amd64" >}}Download for Linux/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}}
|
||||
{{< buttonlink filename="pinniped-cli-linux-amd64" >}}Download {{< latestversion >}} for Linux/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}}
|
||||
|
||||
{{< buttonlink href="https://get.pinniped.dev/latest/pinniped-cli-windows-amd64.exe" >}}Download for Windows/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}}
|
||||
{{< buttonlink filename="pinniped-cli-windows-amd64.exe" >}}Download {{< latestversion >}} for Windows/amd64{{< buttonicon "download.png" >}}{{< /buttonlink >}}
|
||||
|
||||
You should put the command-line tool somewhere on your `$PATH`, such as `/usr/local/bin` on macOS/Linux.
|
||||
You'll also need to mark the file as executable.
|
||||
@@ -36,7 +36,7 @@ To find specific versions or view all available platforms and architectures, vis
|
||||
|
||||
### Gatekeeper
|
||||
|
||||
If you are using macOS, you may get an error dialog when you first run `pinniped` that says `“pinniped” cannot be opened because the developer cannotbe verified`.
|
||||
If you are using macOS, you may get an error dialog when you first run `pinniped` that says `“pinniped” cannot be opened because the developer cannot be verified`.
|
||||
Cancel this dialog, open System Preferences, click Security & Privacy, and click the Allow Anyway button next to the Pinniped message.
|
||||
|
||||
Run the command again and another dialog appears saying `macOS cannot verify the developer of “pinniped”. Are you sure you want to open it?`.
|
||||
@@ -44,12 +44,16 @@ Click Open to allow the command to proceed.
|
||||
|
||||
## Install a specific version via script
|
||||
|
||||
For example, to install v0.9.1 on Linux/amd64:
|
||||
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) and use it to replace the version number in the URL below.
|
||||
|
||||
For example, to install {{< latestversion >}} on Linux/amd64:
|
||||
|
||||
```sh
|
||||
curl -Lso pinniped https://get.pinniped.dev/v0.9.1/pinniped-cli-linux-amd64 \
|
||||
curl -Lso pinniped https://get.pinniped.dev/{{< latestversion >}}/pinniped-cli-linux-amd64 \
|
||||
&& chmod +x pinniped \
|
||||
&& sudo mv pinniped /usr/local/bin/pinniped
|
||||
```
|
||||
|
||||
*Next, [install the Concierge]({{< ref "install-concierge.md" >}})!*
|
||||
## Next steps
|
||||
|
||||
Next, [install the Supervisor]({{< ref "install-supervisor.md" >}}) and/or [install the Concierge]({{< ref "install-concierge.md" >}})!
|
||||
|
||||
@@ -12,51 +12,67 @@ menu:
|
||||
This guide shows you how to install the Pinniped Concierge.
|
||||
You should have a [supported Kubernetes cluster]({{< ref "../reference/supported-clusters" >}}).
|
||||
|
||||
In the examples below, you can replace *{{< latestversion >}}* with your preferred version number.
|
||||
You can find a list of Pinniped releases [on GitHub](https://github.com/vmware-tanzu/pinniped/releases).
|
||||
|
||||
## With default options
|
||||
|
||||
**Warning:** the default Concierge configuration may create a public LoadBalancer Service on your cluster if that is the default on your cloud provider.
|
||||
If you'd prefer to customize the annotations or load balancer IP address, see the "With custom options" section below.
|
||||
|
||||
### Using kapp
|
||||
|
||||
1. Install the latest version of the Concierge into the `pinniped-concierge` namespace with default options using [kapp](https://carvel.dev/kapp/):
|
||||
|
||||
- `kapp deploy --app pinniped-concierge --file https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge.yaml`
|
||||
|
||||
### Using kubectl
|
||||
|
||||
1. Install the latest version of the Concierge CustomResourceDefinitions:
|
||||
|
||||
- `kubectl apply -f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge-crds.yaml`
|
||||
|
||||
This step is required so kubectl can validate the custom resources deployed in the next step.
|
||||
|
||||
1. Install the latest version of the Concierge into the `pinniped-concierge` namespace with default options:
|
||||
|
||||
- `kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml`
|
||||
|
||||
Warning: the default configuration may create a public LoadBalancer Service on your cluster.
|
||||
- `kubectl apply -f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge.yaml`
|
||||
|
||||
## With specific version and default options
|
||||
|
||||
1. Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number and use it to replace the version number in the URL below.
|
||||
|
||||
1. Install the Concierge into the `pinniped-concierge` namespace with default options:
|
||||
|
||||
- `kubectl apply -f https://get.pinniped.dev/v0.9.1/install-pinniped-concierge.yaml`
|
||||
|
||||
*Replace v0.9.1 with your preferred version number.*
|
||||
|
||||
## With custom options
|
||||
|
||||
Pinniped uses [ytt](https://carvel.dev/ytt/) from [Carvel](https://carvel.dev/) as a templating system.
|
||||
|
||||
1. Install the `ytt` command-line tool using the instructions from the [Carvel documentation](https://carvel.dev/#whole-suite).
|
||||
1. Install the `ytt` and `kapp` command-line tools using the instructions from the [Carvel documentation](https://carvel.dev/#whole-suite).
|
||||
|
||||
1. Clone the Pinniped GitHub repository and visit the `deploy/concierge` directory:
|
||||
|
||||
- `git clone git@github.com:vmware-tanzu/pinniped.git`
|
||||
- `cd pinniped/deploy/concierge`
|
||||
|
||||
1. Decide which release version you would like to install. All release versions are [listed on GitHub](https://github.com/vmware-tanzu/pinniped/releases).
|
||||
|
||||
1. Checkout your preferred version tag, e.g. `{{< latestversion >}}`.
|
||||
|
||||
- `git checkout {{< latestversion >}}`
|
||||
|
||||
1. Customize configuration parameters:
|
||||
|
||||
- Edit `values.yaml` with your custom values.
|
||||
- Change the `image_tag` value to match your preferred version tag, e.g. `{{< latestversion >}}`.
|
||||
- See the [default values](http://github.com/vmware-tanzu/pinniped/tree/main/deploy/concierge/values.yaml) for documentation about individual configuration parameters.
|
||||
|
||||
For example, you can change the number of Concierge pods by setting `replicas` or apply custom annotations to the impersonation proxy service using `impersonation_proxy_spec`.
|
||||
|
||||
1. Render templated YAML manifests:
|
||||
|
||||
- `ytt --file .`
|
||||
|
||||
1. Deploy the templated YAML manifests:
|
||||
|
||||
- *If you're using `kubectl`:*
|
||||
- `ytt --file . | kapp deploy --app pinniped-concierge --file -`
|
||||
|
||||
`ytt --file . | kubectl apply -f -`
|
||||
- *If you're using [`kapp` from Carvel](https://carvel.dev/kapp/):*
|
||||
## Next steps
|
||||
|
||||
`ytt --file . | kapp deploy --yes --app pinniped-concierge --diff-changes --file -`
|
||||
|
||||
*Next, configure the Concierge for [JWT]({{< ref "configure-concierge-jwt.md" >}}) or [webhook]({{< ref "configure-concierge-webhook.md" >}}) authentication.*
|
||||
Next, configure the Concierge for
|
||||
[JWT]({{< ref "configure-concierge-jwt.md" >}}) or [webhook]({{< ref "configure-concierge-webhook.md" >}}) authentication,
|
||||
or [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
|
||||
@@ -13,36 +13,44 @@ This guide shows you how to install the Pinniped Supervisor, which allows seamle
|
||||
You should have a supported Kubernetes cluster with working HTTPS ingress capabilities.
|
||||
<!-- TODO: link to support matrix -->
|
||||
|
||||
In the examples below, you can replace *{{< latestversion >}}* with your preferred version number.
|
||||
You can find a list of Pinniped releases [on GitHub](https://github.com/vmware-tanzu/pinniped/releases).
|
||||
|
||||
## With default options
|
||||
|
||||
### Using kapp
|
||||
|
||||
1. Install the latest version of the Supervisor into the `pinniped-supervisor` namespace with default options using [kapp](https://carvel.dev/kapp/):
|
||||
|
||||
- `kapp deploy --app pinniped-supervisor --file https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-supervisor.yaml`
|
||||
|
||||
### Using kubectl
|
||||
|
||||
1. Install the latest version of the Supervisor into the `pinniped-supervisor` namespace with default options:
|
||||
|
||||
- `kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-supervisor.yaml`
|
||||
|
||||
## With specific version and default options
|
||||
|
||||
1. Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number and use it to replace the version number in the URL below.
|
||||
|
||||
1. Install the Supervisor into the `pinniped-supervisor` namespace with default options:
|
||||
|
||||
- `kubectl apply -f https://get.pinniped.dev/v0.9.1/install-pinniped-supervisor.yaml`
|
||||
|
||||
*Replace v0.9.1 with your preferred version number.*
|
||||
- `kubectl apply -f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-supervisor.yaml`
|
||||
|
||||
## With custom options
|
||||
|
||||
Pinniped uses [ytt](https://carvel.dev/ytt/) from [Carvel](https://carvel.dev/) as a templating system.
|
||||
|
||||
1. Install the `ytt` command-line tool using the instructions from the [Carvel documentation](https://carvel.dev/#whole-suite).
|
||||
1. Install the `ytt` and `kapp` command-line tools using the instructions from the [Carvel documentation](https://carvel.dev/#whole-suite).
|
||||
|
||||
1. Clone the Pinniped GitHub repository and visit the `deploy/supervisor` directory:
|
||||
|
||||
- `git clone git@github.com:vmware-tanzu/pinniped.git`
|
||||
- `cd pinniped/deploy/supervisor`
|
||||
|
||||
1. Decide which release version you would like to install. All release versions are [listed on GitHub](https://github.com/vmware-tanzu/pinniped/releases).
|
||||
|
||||
1. Checkout your preferred version tag, e.g. `{{< latestversion >}}`:
|
||||
|
||||
- `git checkout {{< latestversion >}}`
|
||||
|
||||
1. Customize configuration parameters:
|
||||
|
||||
- Edit `values.yaml` with your custom values.
|
||||
- Change the `image_tag` value to match your preferred version tag, e.g. `{{< latestversion >}}`.
|
||||
- See the [default values](http://github.com/vmware-tanzu/pinniped/tree/main/deploy/supervisor/values.yaml) for documentation about individual configuration parameters.
|
||||
|
||||
1. Render templated YAML manifests:
|
||||
@@ -51,13 +59,8 @@ Pinniped uses [ytt](https://carvel.dev/ytt/) from [Carvel](https://carvel.dev/)
|
||||
|
||||
1. Deploy the templated YAML manifests:
|
||||
|
||||
- *If you're using `kubectl`:*
|
||||
`ytt --file . | kapp deploy --app pinniped-supervisor --file -`
|
||||
|
||||
`ytt --file . | kubectl apply -f -`
|
||||
- *If you're using [`kapp` from Carvel](https://carvel.dev/kapp/):*
|
||||
## Next steps
|
||||
|
||||
`ytt --file . | kapp deploy --yes --app pinniped-supervisor --diff-changes --file -`
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have installed the Supervisor, you will want to [configure the Supervisor]({{< ref "configure-supervisor" >}}).
|
||||
Next, [configure the Supervisor as an OIDC issuer]({{< ref "configure-supervisor" >}})!
|
||||
|
||||
138
site/content/docs/howto/login.md
Normal file
138
site/content/docs/howto/login.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: Logging into your cluster using Pinniped
|
||||
description: Logging into your Kubernetes cluster using Pinniped for authentication.
|
||||
cascade:
|
||||
layout: docs
|
||||
menu:
|
||||
docs:
|
||||
name: Log in to a Cluster
|
||||
weight: 500
|
||||
parent: howtos
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This how-to guide assumes that you have already configured the following Pinniped server-side components within your Kubernetes cluster(s):
|
||||
|
||||
1. If you would like to use the Pinniped Supervisor for federated authentication across multiple Kubernetes clusters
|
||||
then you have already:
|
||||
1. [Installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress.
|
||||
1. [Configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}).
|
||||
1. Configured an `OIDCIdentityProvider` or an `LDAPIdentityProvider` for the Supervisor as the source of your user's identities.
|
||||
Various examples of configuring these resources can be found in these guides.
|
||||
1. In each cluster for which you would like to use Pinniped for authentication, you have [installed the Concierge]({{< ref "install-concierge" >}}).
|
||||
1. In each cluster's Concierge, you have configured an authenticator. For example, if you are using the Pinniped Supervisor,
|
||||
then you have configured each Concierge to [use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}).
|
||||
|
||||
You should have also already [installed the `pinniped` command-line]({{< ref "install-cli" >}}) client, which is used to generate Pinniped-compatible kubeconfig files, and is also a `kubectl` plugin to enable the Pinniped-based login flow.
|
||||
|
||||
## Overview
|
||||
|
||||
1. A cluster admin uses Pinniped to generate a kubeconfig for each cluster, and shares the kubeconfig for each cluster with all users of that cluster.
|
||||
1. A cluster user uses `kubectl` with the generated kubeconfig given to them by the cluster admin. `kubectl` interactively prompts the user to log in using their own unique identity.
|
||||
|
||||
## Key advantages of using the Pinniped Supervisor
|
||||
|
||||
Although you can choose to use Pinniped without using the Pinniped Supervisor, there are several key advantages of choosing to use the Pinniped Supervisor to manage identity across fleets of Kubernetes clusters.
|
||||
|
||||
1. A generated kubeconfig for a cluster will be specific for that cluster, however **it will not contain any specific user identity or credentials.
|
||||
This kubeconfig file can be safely shared with all cluster users.** When the user runs `kubectl` commands using this kubeconfig, they will be interactively prompted to log in using their own unique identity from the OIDC or LDAP identity provider configured in the Supervisor.
|
||||
|
||||
1. The Supervisor will provide a federated identity across all clusters that use the same `FederationDomain`.
|
||||
The user will be **prompted by `kubectl` to interactively authenticate once per day**, and then will be able to use all clusters
|
||||
from the same `FederationDomain` for the rest of the day without being asked to authenticate again.
|
||||
This federated identity is secure because behind the scenes the Supervisor is issuing very short-lived credentials
|
||||
that are uniquely scoped to each cluster.
|
||||
|
||||
1. The Supervisor makes it easy to **bring your own OIDC or LDAP identity provider to act as the source of user identities**.
|
||||
It also allows you to configure how identities and group memberships in the OIDC or LDAP identity provider map to identities
|
||||
and group memberships in the Kubernetes clusters.
|
||||
|
||||
## Generate a Pinniped-compatible kubeconfig file
|
||||
|
||||
You will need to generate a Pinniped-compatible kubeconfig file for each cluster in which you have installed the Concierge.
|
||||
This requires admin-level access to each cluster, so this would typically be performed by the same user who installed the Concierge.
|
||||
|
||||
For each cluster, use `pinniped get kubeconfig` to generate the new kubeconfig file for that cluster.
|
||||
|
||||
It is typically sufficient to run this command with no arguments, aside from pointing the command at your admin kubeconfig.
|
||||
The command uses the [same rules](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/)
|
||||
as `kubectl` to find your admin kubeconfig:
|
||||
|
||||
> "By default, `kubectl` looks for a file named config in the `$HOME/.kube` directory. You can specify other kubeconfig files by setting the `KUBECONFIG` environment variable or by setting the `--kubeconfig` flag."
|
||||
|
||||
For example, if your admin `kubeconfig` file were at the path `$HOME/admin-kubeconfig.yaml`, then you could use:
|
||||
|
||||
```sh
|
||||
pinniped get kubeconfig \
|
||||
--kubeconfig "$HOME/admin-kubeconfig.yaml" > pinniped-kubeconfig.yaml
|
||||
```
|
||||
|
||||
The new Pinniped-compatible kubeconfig YAML will be output as stdout, and can be redirected to a file.
|
||||
|
||||
Various default behaviors of `pinniped get kubeconfig` can be overridden using [its command-line options]({{< ref "cli" >}}).
|
||||
|
||||
## Use the generated kubeconfig with `kubectl` to access the cluster
|
||||
|
||||
A cluster user will typically be given a Pinniped-compatible kubeconfig by their cluster admin. They can use this kubeconfig
|
||||
with `kubectl` just like any other kubeconfig, as long as they have also installed the `pinniped` CLI tool at the
|
||||
same absolute path where it is referenced inside the kubeconfig's YAML. The `pinniped` CLI will act as a `kubectl` plugin
|
||||
to manage the user's authentication to the cluster.
|
||||
|
||||
For example, if the kubeconfig were saved at `$HOME/pinniped-kubeconfig.yaml`:
|
||||
|
||||
```bash
|
||||
kubectl get namespaces \
|
||||
--kubeconfig "$HOME/pinniped-kubeconfig.yaml"
|
||||
```
|
||||
|
||||
This command, when configured to use the Pinniped-compatible kubeconfig, will invoke the `pinniped` CLI behind the scenes
|
||||
as an [ExecCredential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins)
|
||||
to authenticate the user to the cluster.
|
||||
|
||||
If the Pinniped Supervisor is used for authentication to that cluster, then the user's authentication experience
|
||||
will depend on which type of identity provider was configured.
|
||||
|
||||
- For an OIDC identity provider, `kubectl` will open the user's web browser and direct it to the login page of
|
||||
their OIDC Provider. This login flow is controlled by the provider, so it may include two-factor authentication or
|
||||
other features provided by the OIDC Provider.
|
||||
|
||||
If the user's browser is not available, then `kubectl` will instead print a URL which can be visited in a
|
||||
browser (potentially on a different computer) to complete the authentication.
|
||||
|
||||
- For an LDAP identity provider, `kubectl` will interactively prompt the user for their username and password at the CLI.
|
||||
|
||||
Alternatively, the user can set the environment variables `PINNIPED_USERNAME` and `PINNIPED_PASSWORD` for the
|
||||
`kubectl` process to avoid the interactive prompts.
|
||||
|
||||
Once the user completes authentication, the `kubectl` command will automatically continue and complete the user's requested command.
|
||||
For the example above, `kubectl` would list the cluster's namespaces.
|
||||
|
||||
## Authorization
|
||||
|
||||
Pinniped provides authentication (usernames and group memberships) but not authorization. Kubernetes authorization is often
|
||||
provided by the [Kubernetes RBAC system](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) on each cluster.
|
||||
|
||||
In the example above, if the user gets an access denied error, then they may need authorization to list namespaces.
|
||||
For example, an admin could grant the user "edit" access to all cluster resources via the user's username:
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding my-user-can-edit \
|
||||
--clusterrole edit \
|
||||
--user my-username@example.com
|
||||
```
|
||||
|
||||
Alternatively, an admin could create role bindings based on the group membership of the users
|
||||
in the upstream identity provider, for example:
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding my-auditors \
|
||||
--clusterrole view \
|
||||
--group auditors
|
||||
```
|
||||
|
||||
## Other notes
|
||||
|
||||
- Temporary session credentials such as ID, access, and refresh tokens are stored in:
|
||||
- `~/.config/pinniped/sessions.yaml` (macOS/Linux)
|
||||
- `%USERPROFILE%/.config/pinniped/sessions.yaml` (Windows).
|
||||
@@ -144,12 +144,16 @@ to authenticate federated identities from the Supervisor.
|
||||
1. Deploy the Pinniped Concierge.
|
||||
|
||||
```sh
|
||||
kubectl apply \
|
||||
--context kind-pinniped-concierge \
|
||||
-f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml
|
||||
kubectl apply --context kind-pinniped-concierge \
|
||||
-f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge-crds.yaml
|
||||
kubectl apply --context kind-pinniped-concierge \
|
||||
-f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge.yaml
|
||||
```
|
||||
|
||||
The `install-pinniped-concierge.yaml` file includes the default deployment options.
|
||||
The `install-pinniped-concierge-crds.yaml` file contains the Concierge CustomResourceDefinitions.
|
||||
These define the custom APIs that you use to configure and interact with the Concierge.
|
||||
|
||||
The `install-pinniped-concierge.yaml` file includes the rest of the Concierge resources with default deployment options.
|
||||
If you would prefer to customize the available options, please see the [Concierge installation guide]({{< ref "../howto/install-concierge" >}})
|
||||
for instructions on how to deploy using `ytt`.
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ as the authenticator.
|
||||
an authenticator that works with your real identity provider, and therefore would not need to deploy or configure local-user-authenticator.
|
||||
|
||||
```sh
|
||||
kubectl apply -f https://get.pinniped.dev/latest/install-local-user-authenticator.yaml
|
||||
kubectl apply -f https://get.pinniped.dev/{{< latestversion >}}/install-local-user-authenticator.yaml
|
||||
```
|
||||
|
||||
The `install-local-user-authenticator.yaml` file includes the default deployment options.
|
||||
@@ -79,8 +79,6 @@ as the authenticator.
|
||||
see [deploy/local-user-authenticator/README.md](https://github.com/vmware-tanzu/pinniped/blob/main/deploy/local-user-authenticator/README.md)
|
||||
for instructions on how to deploy using `ytt`.
|
||||
|
||||
If you prefer to install a specific version, replace `latest` in the URL with the version number such as `v0.9.1`.
|
||||
|
||||
1. Create a test user named `pinny-the-seal` in the local-user-authenticator namespace.
|
||||
|
||||
```sh
|
||||
@@ -101,10 +99,14 @@ as the authenticator.
|
||||
1. Deploy the Pinniped Concierge.
|
||||
|
||||
```sh
|
||||
kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml
|
||||
kubectl apply -f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge-crds.yaml
|
||||
kubectl apply -f https://get.pinniped.dev/{{< latestversion >}}/install-pinniped-concierge.yaml
|
||||
```
|
||||
|
||||
The `install-pinniped-concierge.yaml` file includes the default deployment options.
|
||||
The `install-pinniped-concierge-crds.yaml` file contains the Concierge CustomResourceDefinitions.
|
||||
These define the custom APIs that you use to configure and interact with the Concierge.
|
||||
|
||||
The `install-pinniped-concierge.yaml` file includes the rest of the Concierge resources with default deployment options.
|
||||
If you would prefer to customize the available options, please see the [Concierge installation guide]({{< ref "../howto/install-concierge" >}})
|
||||
for instructions on how to deploy using `ytt`.
|
||||
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
<div class="wrapper">
|
||||
<h2>The Pinniped Project Team:</h2>
|
||||
<div class="grid three">
|
||||
<div class="bio">
|
||||
<div class="image"><img src="/img/andrew-keesler.png" /></div>
|
||||
<div class="info">
|
||||
<p class="name">Andrew Keesler</p>
|
||||
<p class="position">Engineer</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bio">
|
||||
<div class="image"><img src="/img/margo-crawford.png" /></div>
|
||||
<div class="info">
|
||||
@@ -38,9 +31,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="bio">
|
||||
<div class="image"><img src="/img/pablo-schuhmacher.png" /></div>
|
||||
<div class="image"><img src="/img/Anjali-Telang.png" /></div>
|
||||
<div class="info">
|
||||
<p class="name">Pablo Schuhmacher</p>
|
||||
<p class="name">Anjali Telang</p>
|
||||
<p class="position">Product Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
{{- $href := .Get "href" -}}
|
||||
<div class="button"><a href="{{ $href }}" class="button secondary">{{.Inner}}</a></div>
|
||||
{{- $filename := .Get "filename" -}}
|
||||
<div class="button"><a href="https://get.pinniped.dev/{{ .Site.Params.latest_version }}/{{ $filename }}" class="button secondary">{{.Inner}}</a></div>
|
||||
@@ -0,0 +1 @@
|
||||
{{ .Site.Params.latest_version }}
|
||||
BIN
site/themes/pinniped/static/img/Anjali-Telang.png
Normal file
BIN
site/themes/pinniped/static/img/Anjali-Telang.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
@@ -1,6 +1,9 @@
|
||||
# Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
kubernetesDistribution: AKS
|
||||
|
||||
# Describe the capabilities of the cluster against which the integration tests will run.
|
||||
capabilities:
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# The name of the cluster type.
|
||||
kubernetesDistribution: EKS
|
||||
|
||||
# Describe the capabilities of the cluster against which the integration tests will run.
|
||||
capabilities:
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user