From 96137cd0eec0f6243585df0a1ae659eeb2612ab4 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 12 Apr 2022 16:27:42 -0700 Subject: [PATCH 01/77] ldap/ad web ui proposal Signed-off-by: Margo Crawford --- proposals/1113_ldap-ad-web-ui/README.md | 151 ++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 proposals/1113_ldap-ad-web-ui/README.md diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md new file mode 100644 index 00000000..69baaa34 --- /dev/null +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -0,0 +1,151 @@ +--- +title: "Web UI for LDAP/AD login" +authors: [ "@margocrawf" ] +status: "draft" +approval_date: "" +--- + +*Disclaimer*: Proposals are point-in-time designs and decisions. +Once approved and implemented, they become historical documents. +If you are reading an old proposal, please be aware that the +features described herein might have continued to evolve since. + +# Web UI for LDAP/AD login + +## Problem Statement +Today the supervisor only supports a single, hard coded public OAuth client called +"pinniped-cli" which supports the pinniped CLI’s interactions with the Pinniped Supervisor. +When clients log in to their IDPs using LDAP or Active Directory, they are prompted to enter their +credentials the Pinniped CLI without a browser opening. +The pinniped cli sends the client credentials to the Supervisor, which sends them to the identity provider. +The "pinniped-cli" client is privileged and as such is trusted to handle a user's credentials +when authenticating with systems that do not provide an authentication UI (i.e. LDAP). + +However, Pinniped is planning to introduce support for dynamic OAuth clients. +These clients should _not_ be trusted to handle a user's IDP credentials. +Therefore, we need a mechanism for untrusted clients to acquire Pinniped's downstream tokens while +leaving the IDP credential handling to the Pinniped supervisor. + +## Proposal +Pinniped must provide a simple login screen in order to support UIs that wish +to authenticate with the Pinniped Supervisor to gain access to a cluster without +requiring each app to handle IDP credentials. + +### Goals and Non-goals + +#### Goals +* Prevent OAuth clients, other than the Pinniped CLI, from providing credentials via the authorization request +* Provide a minimal feature set (ie user id, password & submit button only) +* Provide generalized error messaging for failed logins that do not expose sensitive information (i.e. we should say "invalid username or password" + but do not expose whether it's the username or password that's incorrect) +* Provide information easily allowing a user to identify the screen as belonging to Pinniped and which upstream IdP is being represented (e.g. IdP name) +* Address basic security concerns for web firms (HTTPS, passwords use a password field, CSRF protection, redirect protection) +* Prevent LDAP injection attacks +* Rely on the upstream IdP to address advanced security concerns (brute force protection, username enumeration, etc) +* Screens are accessible and friendly to screen readers +* Screens are friendly to password managers + +#### Non-goals +* A rich client (ie the use of javascript) +* Advanced UI features (e.g. remember me, reveal password). These features are better left to identity providers to implement. +* Branding & customization beyond the information listed in the goals used to identify the login screen belongs to Pinniped. +* Supporting SSO integrations +* Internationalization or localization. The CLI doesn't currently support this either. + +### Specification / How it Solves the Use Cases + +#### API Changes + +The supervisor must accept requests from other clients, as detailed +in the (todo) proposal for dynamic client registration. +When a client other than pinniped-cli makes an authorization endpoint request with `response_type=code` and their +IDP is an LDAP or Active Directory IDP, the user will be redirected to the new login page. +The login page should display the IDP name and indicate that it belongs to Pinniped. +When a client other than the Pinniped CLI makes an authorization endpoint request with +custom Username/Password headers, they should be rejected. + +The discovery metadata for LDAP/AD IDPS should indicate that they support a flow of `browser_authcode`. + +The pinniped cli should default to using the cli-based password flow, but when a tty is unavailable, +it will open a browser to log in +instead of prompting for username and password. Some users (for example, IDE plugins for kubernetes) +may wish to authenticate using the pinniped cli but without access to a terminal. + +#### Upgrades + +This change is backwards compatible. Users would see no changes unless they decided to register +a new client or change the pinniped cli flags. + +However if they do choose to register a new client they may need to update the following: +- FederationDomains today may be using private certificate authorities. These are trusted + for our use case but a browser will flag them as unsafe. Admins will have to transition to letsencrypt + or another public Certificate Authority to prevent making end users click past messages about the certificate + being untrusted. +- The name of the idp custom resource is currently not published to users logging in with Pinniped. + We plan on exposing this to indicate to users which idp they are logging in to. + Admins may need to update this to something more user-friendly. + +#### Tests + +Chromedriver browser based integration tests will be needed to ensure that a user can log in from a web-based app +by entering their ldap credentials into the web page, as well as unit tests. + +With the pinniped cli: +- succeeds with correct username and password +- fails with incorrect username, shows useful but nonspecific error message +- fails with incorrect password, shows useful but nonspecific error message +- with tty access, prompts for username and password on the cli +- without tty access, opens a browser +- without tty access, if the form post fails, don't ask user to copy and paste the authcode (we already know you have no tty to paste it into...) +Once dynamic clients are implemented: +- fails when attempting to pass username/password as headers on requests to the authorize endpoint +- tests of the rest of the dynamic client functionality that should be detailed as part of that proposal + +#### New Dependencies +This should be kept to a very simple HTML page with minimal, clean CSS styling. +Javascript should be avoided. + +#### Observability Considerations +* Logging login attempts at higher log levels. + +#### Security Considerations +* Preventing LDAP injection attacks: this should be done server-side using our existing + string escaping. +* CSRF protection via a CSRF cookie: this should be similar to the way it is done for the + OIDCIdentityProvider today +* The new UI page must be HTTPS. + +#### Documentation Considerations +This new feature will require documentation to explain how to configure it and to publicise that it is available. +This should include: +* A blog post describing the feature +* Website documentation in the form of a how-to guide + +### Other Approaches Considered +Today, users can configure Dex if they want a web-based LDAP login. +This introduces complexity because they have to install, configure and +maintain both Pinniped and Dex in order to use this feature. It also means +that users do not benefit from the opinionated `ActiveDirectoryIdentityProvider` +config because Dex does not have an equivalent. + +## Open Questions +* What is the format for the URL? (`issuer/some/path`? Something else?) Can we make it so we can reuse the existing cert, + or will we need a new wildcard cert? +* Currently we have little validation on branding requirements. Is specifying the idp name enough for users to understand + how to log in? +* How many users will be blocked on using this feature until they can have a company name and logo on the login page? +* Should we allow admins or users to decide to use the web ui with the pinniped cli, or is it sufficient for us to + determine it based on presence/absence of tty? + +## Implementation Plan +While this work is intended to supplement the dynamic client work, parts of it +can be implemented independently. +The pinniped cli can support a web based ui flow via a command line flag, environment variable or checking whether a tty is available. +Then once dynamic clients exist, we can add functionality to accept requests +from those clients as well. + +## Implementation PRs +This section is a placeholder to list the PRs that implement this proposal. +This section should be left empty until after the proposal is approved. +After implementation, the proposal can be updated to list related +implementation PRs. From a3f7afaec4a2bd78bc9c38c85670ec6eeff009a7 Mon Sep 17 00:00:00 2001 From: hectorj2f Date: Mon, 18 Apr 2022 01:06:59 +0200 Subject: [PATCH 02/77] oidc: add code challenge supported methods Signed-off-by: hectorj2f --- internal/oidc/discovery/discovery_handler.go | 2 ++ internal/oidc/discovery/discovery_handler_test.go | 1 + test/integration/supervisor_discovery_test.go | 1 + 3 files changed, 4 insertions(+) diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/oidc/discovery/discovery_handler.go index ca7fdd2b..8adf2350 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/oidc/discovery/discovery_handler.go @@ -37,6 +37,7 @@ type Metadata struct { TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` ScopesSupported []string `json:"scopes_supported"` ClaimsSupported []string `json:"claims_supported"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` // ^^^ Optional ^^^ @@ -64,6 +65,7 @@ func NewHandler(issuerURL string) http.Handler { SubjectTypesSupported: []string{"public"}, IDTokenSigningAlgValuesSupported: []string{"ES256"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, + CodeChallengeMethodsSupported: []string{"S256"}, ScopesSupported: []string{"openid", "offline"}, ClaimsSupported: []string{"groups"}, } diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/oidc/discovery/discovery_handler_test.go index 855f830d..293cdad4 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/oidc/discovery/discovery_handler_test.go @@ -46,6 +46,7 @@ func TestDiscovery(t *testing.T) { "id_token_signing_alg_values_supported": ["ES256"], "token_endpoint_auth_methods_supported": ["client_secret_basic"], "scopes_supported": ["openid", "offline"], + "code_challenge_methods_supported": ["S256"], "claims_supported": ["groups"], "discovery.supervisor.pinniped.dev/v1alpha1": { "pinniped_identity_providers_endpoint": "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers" diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 9991fa01..2d828e4c 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -505,6 +505,7 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso "scopes_supported": ["openid", "offline"], "response_types_supported": ["code"], "response_modes_supported": ["query", "form_post"], + "code_challenge_methods_supported": ["S256"], "claims_supported": ["groups"], "discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"}, "subject_types_supported": ["public"], From fb8083d024d025f33b23a26113871ee69e351c90 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 19 Apr 2022 11:09:24 -0700 Subject: [PATCH 03/77] bump some direct deps --- go.mod | 20 +-- go.sum | 117 ++++-------------- .../formposthtml/formposthtml_test.go | 6 +- 3 files changed, 34 insertions(+), 109 deletions(-) diff --git a/go.mod b/go.mod index f998c9e6..8c6cfbf4 100644 --- a/go.mod +++ b/go.mod @@ -39,11 +39,11 @@ replace ( require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/coreos/go-oidc/v3 v3.1.0 - github.com/creack/pty v1.1.17 + github.com/creack/pty v1.1.18 github.com/davecgh/go-spew v1.1.1 github.com/felixge/httpsnoop v1.0.2 - github.com/go-ldap/ldap/v3 v3.4.2 - github.com/go-logr/logr v1.2.2 + github.com/go-ldap/ldap/v3 v3.4.3 + github.com/go-logr/logr v1.2.3 github.com/go-logr/stdr v1.2.2 github.com/gofrs/flock v0.8.1 github.com/golang/mock v1.6.0 @@ -54,17 +54,17 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/ory/fosite v0.42.1 + github.com/ory/fosite v0.42.2 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 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.3.0 + github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.0 - github.com/tdewolff/minify/v2 v2.10.0 + github.com/stretchr/testify v1.7.1 + github.com/tdewolff/minify/v2 v2.11.1 go.uber.org/atomic v1.9.0 - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b golang.org/x/sync v0.0.0-20210220032951-036812b2e83c @@ -110,7 +110,7 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/swag v0.21.1 // indirect @@ -154,7 +154,7 @@ require ( github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/tdewolff/parse/v2 v2.5.27 // indirect + github.com/tdewolff/parse/v2 v2.5.28 // indirect go.etcd.io/etcd/api/v3 v3.5.2 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect go.etcd.io/etcd/client/v3 v3.5.2 // indirect diff --git a/go.sum b/go.sum index 035be56a..c4ae3d2a 100644 --- a/go.sum +++ b/go.sum @@ -29,7 +29,6 @@ cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aD cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= @@ -46,7 +45,6 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m 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/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -77,14 +75,12 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= @@ -116,7 +112,6 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m 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= -github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -145,7 +140,6 @@ github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -158,8 +152,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -168,9 +160,7 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= @@ -210,8 +200,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t 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.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/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= @@ -257,15 +247,12 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -286,9 +273,8 @@ github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49P github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-asn1-ber/asn1-ber v1.5.3 h1:u7utq56RUFiynqUzgVMFDymapcOtQ/MZkh3H4QYkxag= -github.com/go-asn1-ber/asn1-ber v1.5.3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -296,16 +282,17 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.4.2 h1:zFZKcXKLqZpFMrMQGHeHWKXbDTdNCmhGY9AK41zPh+8= -github.com/go-ldap/ldap/v3 v3.4.2/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= +github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI= +github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= @@ -772,25 +759,15 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t 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/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -798,21 +775,13 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -876,7 +845,6 @@ github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65 github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -928,7 +896,6 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/luna-duclos/instrumentedsql v0.0.0-20181127104832-b7d587d28109/go.mod h1:PWUIzhtavmOR965zfawVsHXbEuU1G29BPZ/CB3C7jXk= 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/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -971,18 +938,14 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -997,10 +960,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -1081,8 +1041,8 @@ github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnh github.com/ory/dockertest/v3 v3.5.4/go.mod h1:J8ZUbNB2FOhm1cFZW9xBpDsODqsSWcyYgtJYVPcnF70= github.com/ory/dockertest/v3 v3.6.3/go.mod h1:EFLcVUOl8qCwp9NyDAcCDtq/QviLtYswW/VbWzUnTNE= github.com/ory/fosite v0.29.0/go.mod h1:0atSZmXO7CAcs6NPMI/Qtot8tmZYj04Nddoold4S2h0= -github.com/ory/fosite v0.42.1 h1:h2j+namO6bZIaD4WAFFHx+Cwfc8EDATE5JWQR9NMo6c= -github.com/ory/fosite v0.42.1/go.mod h1:qggrqm3ZWQF9i2f/d3RLH5mHHPtv44hsiltkVKLsCYo= +github.com/ory/fosite v0.42.2 h1:fKfGAgMmmeM1C0DXCyt5TOzQWrKmLOL+PApEC4bIv2o= +github.com/ory/fosite v0.42.2/go.mod h1:qggrqm3ZWQF9i2f/d3RLH5mHHPtv44hsiltkVKLsCYo= github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90/go.mod h1:sxnvPCxChFuSmTJGj8FdMupeq1BezCiEpDjTUXQ4hf4= github.com/ory/go-acc v0.2.6/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw= github.com/ory/go-acc v0.2.7 h1:VjSz+Xj3LoJRBmAXt9cxuL1lanO/Bvf9ajny5NvwbtM= @@ -1108,7 +1068,6 @@ github.com/ory/x v0.0.214 h1:nz5ijvm5MVhYxWsQSuUrW1hj9F5QLZvPn/nLo5s06T4= github.com/ory/x v0.0.214/go.mod h1:aRl57gzyD4GF0HQCekovXhv0xTZgAgiht3o8eVhsm9Q= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -1137,14 +1096,12 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= @@ -1157,7 +1114,6 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= @@ -1168,7 +1124,6 @@ github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= @@ -1197,7 +1152,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/santhosh-tekuri/jsonschema/v2 v2.1.0/go.mod h1:yzJzKUGV4RbWqWIBBP4wSOBqavX5saE02yirLS0OTyg= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1252,7 +1206,6 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B 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/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk= github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= @@ -1270,8 +1223,8 @@ github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= -github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 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= @@ -1286,7 +1239,6 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM 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/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= 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/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1299,15 +1251,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.10.0 h1:ovVAHUcjfGrBDf1EIvsodRUVJiZK/28mMose08B7k14= -github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM= -github.com/tdewolff/parse/v2 v2.5.27 h1:PL3LzzXaOpmdrknnOlIeO2muIBHAwiKp6TxN1RbU5gI= -github.com/tdewolff/parse/v2 v2.5.27/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/minify/v2 v2.11.1 h1:x2IAGnHs3qBjulArA7g4dYGCpcMrM8H2sywfwr436RA= +github.com/tdewolff/minify/v2 v2.11.1/go.mod h1:UkCTT2Sa8N7XNU0Z9Q+De6NvaxPlC7DGfSWDRowwXqY= +github.com/tdewolff/parse/v2 v2.5.28 h1:QziFVLe+bfFIwnCWAJzMrzwltQXPT21Evl9Z4x25D+U= +github.com/tdewolff/parse/v2 v2.5.28/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= @@ -1324,7 +1277,6 @@ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= @@ -1359,16 +1311,13 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.2 h1:tXok5yLlKyuQ/SXSjtqHc4uzNaMqZi2XsoSPr/LlJXI= go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.2 h1:4hzqQ6hIb3blLyQ8usCU4h3NghkqcsohEQ3o3VetYxE= go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v2 v2.305.1 h1:vtxYCKWA9x31w0WJj7DdqsHFNjhkigdAnziDtkZb/l4= -go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.etcd.io/etcd/client/v3 v3.5.2 h1:WdnejrUtQC4nCxK0/dLTMqKOB+U5TP/2Ya0BJL+1otA= go.etcd.io/etcd/client/v3 v3.5.2/go.mod h1:kOOaWFFgHygyT0WlSmL8TJiXmMysO/nNUlEsSsN6W4o= @@ -1464,14 +1413,12 @@ golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -1480,8 +1427,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1522,7 +1469,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1556,7 +1502,6 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1586,12 +1531,10 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1616,7 +1559,6 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= @@ -1675,11 +1617,8 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1718,7 +1657,6 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1737,15 +1675,11 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1827,7 +1761,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1925,9 +1858,7 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= @@ -2004,11 +1935,7 @@ google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -2048,7 +1975,6 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -2090,7 +2016,6 @@ 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/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= diff --git a/internal/oidc/provider/formposthtml/formposthtml_test.go b/internal/oidc/provider/formposthtml/formposthtml_test.go index 94530a73..07fb508a 100644 --- a/internal/oidc/provider/formposthtml/formposthtml_test.go +++ b/internal/oidc/provider/formposthtml/formposthtml_test.go @@ -29,7 +29,7 @@ var ( - + @@ -56,13 +56,13 @@ var ( - `) + `) // 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-1LS3gM7wTGc0dYXZiqW6HK1LHk74YSG8GsJBC/j1/i8='; ` + - `style-src 'sha256-CtfkX7m8x2UdGYvGgDq+6b6yIAQsASW9pbQK+sG8fNA='; ` + + `style-src 'sha256-kXh6OrB2z7wkx7v1N3ay9deQhV5edwuogARaUtvNYN4='; ` + `img-src data:; ` + `connect-src *; ` + `frame-ancestors 'none'` From 5b9831d3194275a98ca32d23fd1f89123b1dfe9d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 19 Apr 2022 11:13:52 -0700 Subject: [PATCH 04/77] bump the kube direct deps --- go.mod | 16 ++++++++-------- go.sum | 35 +++++++++++++++++------------------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 8c6cfbf4..47a6be97 100644 --- a/go.mod +++ b/go.mod @@ -71,15 +71,15 @@ require ( golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/text v0.3.7 gopkg.in/square/go-jose.v2 v2.6.0 - k8s.io/api v0.23.4 - k8s.io/apiextensions-apiserver v0.23.4 - k8s.io/apimachinery v0.23.4 - k8s.io/apiserver v0.23.4 - k8s.io/client-go v0.23.4 - k8s.io/component-base v0.23.4 + k8s.io/api v0.23.5 + k8s.io/apiextensions-apiserver v0.23.5 + k8s.io/apimachinery v0.23.5 + k8s.io/apiserver v0.23.5 + k8s.io/client-go v0.23.5 + k8s.io/component-base v0.23.5 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 k8s.io/klog/v2 v2.40.1 - k8s.io/kube-aggregator v0.23.4 + k8s.io/kube-aggregator v0.23.5 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/yaml v1.3.0 ) @@ -186,7 +186,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.28 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) diff --git a/go.sum b/go.sum index c4ae3d2a..ee8a7c9c 100644 --- a/go.sum +++ b/go.sum @@ -2058,19 +2058,19 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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.23.4 h1:85gnfXQOWbJa1SiWGpE9EEtHs0UVvDyIsSMpEtl2D4E= -k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= -k8s.io/apiextensions-apiserver v0.23.4 h1:AFDUEu/yEf0YnuZhqhIFhPLPhhcQQVuR1u3WCh0rveU= -k8s.io/apiextensions-apiserver v0.23.4/go.mod h1:TWYAKymJx7nLMxWCgWm2RYGXHrGlVZnxIlGnvtfYu+g= -k8s.io/apimachinery v0.23.4 h1:fhnuMd/xUL3Cjfl64j5ULKZ1/J9n8NuQEgNL+WXWfdM= -k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apiserver v0.23.4 h1:zNvQlG+C/ERjuUz4p7eY/0IWHaMixRSBoxgmyIdwo9Y= -k8s.io/apiserver v0.23.4/go.mod h1:A6l/ZcNtxGfPSqbFDoxxOjEjSKBaQmE+UTveOmMkpNc= -k8s.io/client-go v0.23.4 h1:YVWvPeerA2gpUudLelvsolzH7c2sFoXXR5wM/sWqNFU= -k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= -k8s.io/code-generator v0.23.4/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/component-base v0.23.4 h1:SziYh48+QKxK+ykJ3Ejqd98XdZIseVBG7sBaNLPqy6M= -k8s.io/component-base v0.23.4/go.mod h1:8o3Gg8i2vnUXGPOwciiYlkSaZT+p+7gA9Scoz8y4W4E= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= +k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apiserver v0.23.5 h1:2Ly8oUjz5cnZRn1YwYr+aFgDZzUmEVL9RscXbnIeDSE= +k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= +k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= +k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= +k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= +k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -2079,8 +2079,8 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.40.1 h1:P4RRucWk/lFOlDdkAr3mc7iWFkgKrZY9qZMAgek06S4= k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.23.4 h1:gLk78rGLVfUXCdD14NrKg/JFBmNNCZ8FEs3tYt+W6Zk= -k8s.io/kube-aggregator v0.23.4/go.mod h1:hpmPi4oaLBe014CkBCqzBYWok64H2C7Ka6FBLJvHgkg= +k8s.io/kube-aggregator v0.23.5 h1:UZ+qE3hGo6DcgKySf27Jg7d3X9/6JQkVLUiHZAoAfCY= +k8s.io/kube-aggregator v0.23.5/go.mod h1:3ynYx07Co6dzjpKPgipM+1/Mt2Jcm7dY++cRlKLr5s8= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf h1:M9XBsiMslw2lb2ZzglC0TOkBPK5NQi0/noUrdnoFwUg= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= @@ -2097,9 +2097,8 @@ 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.27/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.28 h1:W77Zf0W3/x9IuyysjbDMV9wJKH/ksG6eviSzyd4jFTc= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.28/go.mod h1:tq2nT0Kx7W+/f2JVE+zxYtUhdjuELJkVpNz+x/QN5R4= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 h1:dUk62HQ3ZFhD48Qr8MIXCiKA8wInBQCtuE4QGfFW7yA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= From 9e5d4ae51c0fda4b5371236312ac2989d183b28e Mon Sep 17 00:00:00 2001 From: Anjali Telang Date: Thu, 14 Apr 2022 09:45:54 -0400 Subject: [PATCH 05/77] Blog for v0.16.0 Signed-off-by: Anjali Telang --- .../content/posts/2022-04-15-fips-and-more.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 site/content/posts/2022-04-15-fips-and-more.md diff --git a/site/content/posts/2022-04-15-fips-and-more.md b/site/content/posts/2022-04-15-fips-and-more.md new file mode 100644 index 00000000..ac1c5110 --- /dev/null +++ b/site/content/posts/2022-04-15-fips-and-more.md @@ -0,0 +1,65 @@ +--- +title: "Pinniped v0.16.0: With Build-Your-Own FIPS Binaries, Workspace ONE IDP configuration, and Supervisor HTTP listener changes" +slug: fips-and-more +date: 2022-04-14 +author: Anjali Telang +image: https://images.unsplash.com/photo-1618075254478-850bc1729c17?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2274&q=80 +excerpt: "With the release of v0.16.0 , You can now build your own Pinniped binaries with FIPS compliant BoringCrypto, HTTPS will be the default for our public facing Supervisor listener ports, and we provide you with documentation to configure Workspace ONE Access as an OIDC Identity Provider" +tags: ['Margo Crawford','Ryan Richard', 'Mo Khan', 'Anjali Telang', 'release'] +--- + +![happy seal](https://images.unsplash.com/photo-1618075254478-850bc1729c17?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2274&q=80) +*Photo by [karlheinz_eckhardt](https://unsplash.com/@karlheinz_eckhardt) on [Unsplash](https://unsplash.com/s/photos/seal)* + +This release continues our theme of providing security-hardening for Kubernetes authentication solutions with Pinniped. + +# Build-Your-Own FIPS compliant Pinniped Binaries + +We now bring to you information on how to Build-Your-Own Pinniped binaries with FIPS Compliant BoringSSL Crypto. The [Federal Information Processing Standard](https://csrc.nist.gov/publications/detail/fips/140/2/final) (FIPS) 140-2 publication describes United States government approved security requirements for cryptographic modules. Software that is validated by an accredited Cryptographic Module Validation Program (CVMP) laboratory can be suitable for use in applications for US governmental departments or in industries subject to US Federal regulations. + +Refer to our [FIPS reference documentation]({{< ref "docs/reference/fips.md" >}}) that provides details on how to compile Pinniped with a FIPS validated cryptographic module that adheres to the standards established by FIPS 140-2. We are using [BoringSSL/BoringCrypto](https://github.com/golang/go/blob/dev.boringcrypto/misc/boring/README.md) in our example with an open source BoringCrypto flavor of Go readily available as the cryptographic module. BoringSSL is Google’s fork of OpenSSL and as a whole is not FIPS validated, but a specific core library called BoringCrypto is. For more detailed information about BoringCrypto see [here](https://boringssl.googlesource.com/boringssl/+/master/crypto/fipsmodule/FIPS.md). + +**Note: We will not provide official support for FIPS configuration, and may not respond to GitHub issues opened related to FIPS support. Out intent is to provide you with an example of how you can do this yourself in your environments.** + +## Supervisor with default HTTPS listener port + +With [v0.13.0](https://github.com/vmware-tanzu/pinniped/releases/tag/v0.13.0) we had announced that we will disable the use of default HTTP listeners. + +**Breaking change in this release: With this release, we disable the Supervisor's HTTP listener by default, and will not allow it to be configured to bind to anything other than loopback interfaces.** + +However, we do recognize that it may take some users time to adjust to this breaking change. If you want to bring back the insecure http listen ports behavior into your deployments, you can set the variable, ***deprecated_insecure_accept_external_unencrypted_http_requests***, in your Supervisor deployment yaml file. This will print a warning in the pod logs that lets you know that this is an insecure option. Please do note that we plan to remove this field in some future release and only provide you with secure https options. This deprecated field will be available for at least two releases to give users time to make changes. + +This feature does not change any HTTPS listen port configuration nor does it change the user's ability to do the following with the HTTP listener ports: + +1. Enable or disable the HTTP listening port +2. Configure the HTTP listening port to listen on tcp loopback interfaces (ipv4, ipv6, or both) or on a unix domain socket file for listening for connections from inside the pod, for example connections from a service mesh's sidecar container +3. Choose the port number for the HTTP listening port + +For more information on this feature refer to [#981](https://github.com/vmware-tanzu/pinniped/issues/981). + +## Workspace ONE Identity Provider configuration + +We continue to gather feedback from the community around the need to integrate with different Identity Providers. With this in mind, we have documented our support for configuring [VMware Workspace ONE Access](https://www.vmware.com/products/workspace-one/access.html) (formerly VMware Identity Manager) as an Identity provider. Workspace ONE access also acts as a broker to other identity stores and providers—including Active Directory (AD), Active Directory Federation Services (ADFS), Azure AD, Okta and Ping Identity to enable authentication across on-premises, software-as-a-service (SaaS), web and native applications. Available as a cloud-hosted service, Workspace ONE Access is an integral part of the Workspace ONE platform. + +Refer to our detailed guide on [how to configure supervisor with Workspace ONE Access]({{< ref "docs/howto/configure-supervisor-with-workspace_one_access.md" >}}). + +## What else is in this release? + +In addition to the above features, this release also adds custom prefixes to Supervisor authcodes, access tokens, and refresh tokens. The prefixes are intended to make the tokens more identifiable to a user when seen out of context. The prefixes are `pin_ac_` for authcodes, `pin_at_` for access tokens, and `pin_rt_` for refresh tokens. See [#688](https://github.com/vmware-tanzu/pinniped/issues/688) for more on this. +Refer to the [release notes for v0.16.0](https://github.com/vmware-tanzu/pinniped/releases/tag/v0.16.0) for a complete list of fixes and features included in the release. + +## Community contributors + +The Pinniped community continues to grow, and is a vital part of the project's success. This release includes contributions from users [@vicmarbev](https://github.com/vicmarbev) and [@hectorj2f](https://github.com/hectorj2f). Thank you for helping improve Pinniped! + +[Are you using Pinniped?](https://github.com/vmware-tanzu/pinniped/discussions/152) +Did you try our new security hardening features? +Are there other Identity Providers for which you want to see documentation similar to what we provided for Workspace ONE Access? + +We thrive on community feedback and would like to hear more! + +Reach out to us in [#pinniped](https://kubernetes.slack.com/archives/C01BW364RJA) on Kubernetes Slack, +[create an issue](https://github.com/vmware-tanzu/pinniped/issues/new/choose) on our Github repository, +or start a [discussion](https://github.com/vmware-tanzu/pinniped/discussions). + +{{< community >}} From 019750a292900e9a82b2cbcb1d29768547c1b226 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 19 Apr 2022 10:48:48 -0700 Subject: [PATCH 06/77] Update kube versions to latest patch Signed-off-by: Margo Crawford --- generated/1.20/apis/go.mod | 4 ++-- generated/1.20/apis/go.sum | 8 ++++---- generated/1.20/client/go.mod | 4 ++-- generated/1.20/client/go.sum | 12 ++++++------ generated/1.21/apis/go.mod | 4 ++-- generated/1.21/apis/go.sum | 8 ++++---- generated/1.21/client/go.mod | 4 ++-- generated/1.21/client/go.sum | 12 ++++++------ generated/1.22/apis/go.mod | 4 ++-- generated/1.22/apis/go.sum | 8 ++++---- generated/1.22/client/go.mod | 4 ++-- generated/1.22/client/go.sum | 16 ++++++++-------- generated/1.23/apis/go.mod | 4 ++-- generated/1.23/apis/go.sum | 8 ++++---- generated/1.23/client/go.mod | 4 ++-- generated/1.23/client/go.sum | 12 ++++++------ hack/lib/kube-versions.txt | 8 ++++---- 17 files changed, 62 insertions(+), 62 deletions(-) diff --git a/generated/1.20/apis/go.mod b/generated/1.20/apis/go.mod index 6eee615b..6c7a685e 100644 --- a/generated/1.20/apis/go.mod +++ b/generated/1.20/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.20/apis go 1.13 require ( - k8s.io/api v0.20.14 - k8s.io/apimachinery v0.20.14 + k8s.io/api v0.20.15 + k8s.io/apimachinery v0.20.15 ) diff --git a/generated/1.20/apis/go.sum b/generated/1.20/apis/go.sum index b252f0b3..187861b1 100644 --- a/generated/1.20/apis/go.sum +++ b/generated/1.20/apis/go.sum @@ -192,10 +192,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.20.14 h1:vAI3AdDY0Ou9oAkOy/fQ3K0F+FOT+TyZqykKGKHJYQA= -k8s.io/api v0.20.14/go.mod h1:l/ErofD0cbemY3VGAuqcyQFu0+FoSYD1sOAi6PwUCis= -k8s.io/apimachinery v0.20.14 h1:LG7YY3R3ZRO5UxaIsInDk8adAb9J744CP2EfckAIM7w= -k8s.io/apimachinery v0.20.14/go.mod h1:4KFiDSxCoGviCiRk9kTXIROsIf4VSGkVYjVJjJln3pg= +k8s.io/api v0.20.15 h1:7PoPWNuE/pFFhMIQCuto88+63TIjSlCviXknxWCHLVs= +k8s.io/api v0.20.15/go.mod h1:X3JDf1BiTRQQ6xNAxTuhgi6yL2dHc6fSr9LGzE+Z3YU= +k8s.io/apimachinery v0.20.15 h1:tZW9jhDILQJq0fYXq7/t0xulj+73HzxLVBUGLCNg9uM= +k8s.io/apimachinery v0.20.15/go.mod h1:4KFiDSxCoGviCiRk9kTXIROsIf4VSGkVYjVJjJln3pg= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= diff --git a/generated/1.20/client/go.mod b/generated/1.20/client/go.mod index 9d068aae..5888f3a2 100644 --- a/generated/1.20/client/go.mod +++ b/generated/1.20/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.20/apis v0.0.0 - k8s.io/apimachinery v0.20.14 - k8s.io/client-go v0.20.14 + k8s.io/apimachinery v0.20.15 + k8s.io/client-go v0.20.15 ) replace go.pinniped.dev/generated/1.20/apis => ../apis diff --git a/generated/1.20/client/go.sum b/generated/1.20/client/go.sum index a29c2c00..817a272a 100644 --- a/generated/1.20/client/go.sum +++ b/generated/1.20/client/go.sum @@ -410,12 +410,12 @@ 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= -k8s.io/api v0.20.14 h1:vAI3AdDY0Ou9oAkOy/fQ3K0F+FOT+TyZqykKGKHJYQA= -k8s.io/api v0.20.14/go.mod h1:l/ErofD0cbemY3VGAuqcyQFu0+FoSYD1sOAi6PwUCis= -k8s.io/apimachinery v0.20.14 h1:LG7YY3R3ZRO5UxaIsInDk8adAb9J744CP2EfckAIM7w= -k8s.io/apimachinery v0.20.14/go.mod h1:4KFiDSxCoGviCiRk9kTXIROsIf4VSGkVYjVJjJln3pg= -k8s.io/client-go v0.20.14 h1:DAtFSq905IE49N/WOzI1PvwnifI6Vduti5v8A2xJEt8= -k8s.io/client-go v0.20.14/go.mod h1:NP3va0ehKLBNmXBUIQD6ddTvK7Pu/wioGuitv++pYow= +k8s.io/api v0.20.15 h1:7PoPWNuE/pFFhMIQCuto88+63TIjSlCviXknxWCHLVs= +k8s.io/api v0.20.15/go.mod h1:X3JDf1BiTRQQ6xNAxTuhgi6yL2dHc6fSr9LGzE+Z3YU= +k8s.io/apimachinery v0.20.15 h1:tZW9jhDILQJq0fYXq7/t0xulj+73HzxLVBUGLCNg9uM= +k8s.io/apimachinery v0.20.15/go.mod h1:4KFiDSxCoGviCiRk9kTXIROsIf4VSGkVYjVJjJln3pg= +k8s.io/client-go v0.20.15 h1:B6Wvl5yFiHkDZaZ0i5Vju6mGHw4Zo2DzDE8XF378Asc= +k8s.io/client-go v0.20.15/go.mod h1:q/vywQFfGT3jw+lXQGA9sEJDH0QEX7XUT2PwrQ2qm/I= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= diff --git a/generated/1.21/apis/go.mod b/generated/1.21/apis/go.mod index fc37ecb6..8db9293f 100644 --- a/generated/1.21/apis/go.mod +++ b/generated/1.21/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.21/apis go 1.13 require ( - k8s.io/api v0.21.9 - k8s.io/apimachinery v0.21.9 + k8s.io/api v0.21.10 + k8s.io/apimachinery v0.21.10 ) diff --git a/generated/1.21/apis/go.sum b/generated/1.21/apis/go.sum index d766e253..d1e7b7d0 100644 --- a/generated/1.21/apis/go.sum +++ b/generated/1.21/apis/go.sum @@ -147,10 +147,10 @@ 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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.21.9 h1:dgxM5d8/kLw0mz7JmyixJk3I84JT2B52Yz8p0lTMFes= -k8s.io/api v0.21.9/go.mod h1:jyTBdRcQnzZodHyJdeDEqVcxkaqJAgjrRx30EysE1Ik= -k8s.io/apimachinery v0.21.9 h1:8WffZaaNB2ft5wOiFPktkZRZQxMoTxwVrITC73SJ1V8= -k8s.io/apimachinery v0.21.9/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= +k8s.io/api v0.21.10 h1:WKcYyNBZNMrE9yejBs0Lx70jGsOW8uUwkiA4ioxkz1Q= +k8s.io/api v0.21.10/go.mod h1:5kqv2pCXwcrOvV12WhVAtLZUKaM0kyrZ6nHObw8SojA= +k8s.io/apimachinery v0.21.10 h1:mOStSZoCrsxnAMIm5UtCNn6P328cJAhtzJToQYFsylc= +k8s.io/apimachinery v0.21.10/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.21/client/go.mod b/generated/1.21/client/go.mod index 436407d4..4222b11d 100644 --- a/generated/1.21/client/go.mod +++ b/generated/1.21/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.21/apis v0.0.0 - k8s.io/apimachinery v0.21.9 - k8s.io/client-go v0.21.9 + k8s.io/apimachinery v0.21.10 + k8s.io/client-go v0.21.10 ) replace go.pinniped.dev/generated/1.21/apis => ../apis diff --git a/generated/1.21/client/go.sum b/generated/1.21/client/go.sum index d747a3a3..cf649bbd 100644 --- a/generated/1.21/client/go.sum +++ b/generated/1.21/client/go.sum @@ -402,12 +402,12 @@ 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= -k8s.io/api v0.21.9 h1:dgxM5d8/kLw0mz7JmyixJk3I84JT2B52Yz8p0lTMFes= -k8s.io/api v0.21.9/go.mod h1:jyTBdRcQnzZodHyJdeDEqVcxkaqJAgjrRx30EysE1Ik= -k8s.io/apimachinery v0.21.9 h1:8WffZaaNB2ft5wOiFPktkZRZQxMoTxwVrITC73SJ1V8= -k8s.io/apimachinery v0.21.9/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= -k8s.io/client-go v0.21.9 h1:GexEazmr/ulHLNBKDE/pc2WTbZ0JLUJLv05Va9kE/B0= -k8s.io/client-go v0.21.9/go.mod h1:uMq9B14yobLb20bDZ1xVrXUpPbDCeWEjJfGeTt2n0/Q= +k8s.io/api v0.21.10 h1:WKcYyNBZNMrE9yejBs0Lx70jGsOW8uUwkiA4ioxkz1Q= +k8s.io/api v0.21.10/go.mod h1:5kqv2pCXwcrOvV12WhVAtLZUKaM0kyrZ6nHObw8SojA= +k8s.io/apimachinery v0.21.10 h1:mOStSZoCrsxnAMIm5UtCNn6P328cJAhtzJToQYFsylc= +k8s.io/apimachinery v0.21.10/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= +k8s.io/client-go v0.21.10 h1:/AKJEgLpQDWvZbq7cq2vEx0bpqpAlOOHitOrctSV8bI= +k8s.io/client-go v0.21.10/go.mod h1:nAGhVCjwhbDP2whk65n3STSCn24H/VGp1pKSk9UszU8= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.22/apis/go.mod b/generated/1.22/apis/go.mod index 63226535..3505f894 100644 --- a/generated/1.22/apis/go.mod +++ b/generated/1.22/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.22/apis go 1.13 require ( - k8s.io/api v0.22.6 - k8s.io/apimachinery v0.22.6 + k8s.io/api v0.22.8 + k8s.io/apimachinery v0.22.8 ) diff --git a/generated/1.22/apis/go.sum b/generated/1.22/apis/go.sum index e2526f09..11f00878 100644 --- a/generated/1.22/apis/go.sum +++ b/generated/1.22/apis/go.sum @@ -205,10 +205,10 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.22.6 h1:acjE5ABt0KpsBI9QCtLqaQEPSF94jOtE/LoFxSYasSE= -k8s.io/api v0.22.6/go.mod h1:q1F7IfaNrbi/83ebLy3YFQYLjPSNyunZ/IXQxMmbwCg= -k8s.io/apimachinery v0.22.6 h1:z7vxNRkFX0NToA+8D17kzLZ/T4t+DqwzUlqqbqRepRs= -k8s.io/apimachinery v0.22.6/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= +k8s.io/api v0.22.8 h1:7Ld6tHuvaYzcQE2axLmomWlhP0fK3vpLfo6fBaNrCIs= +k8s.io/api v0.22.8/go.mod h1:uLlWJNRJ+AYwgAdsNwf0TsD3eByNYW9RlXFmkMdL3yk= +k8s.io/apimachinery v0.22.8 h1:kazMo4/t5ZPI7MwImnCJODZrt1VuwbYBixhTzaNIxsw= +k8s.io/apimachinery v0.22.8/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.22/client/go.mod b/generated/1.22/client/go.mod index 9eb8a759..5dbb6a8a 100644 --- a/generated/1.22/client/go.mod +++ b/generated/1.22/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.22/apis v0.0.0 - k8s.io/apimachinery v0.22.6 - k8s.io/client-go v0.22.6 + k8s.io/apimachinery v0.22.8 + k8s.io/client-go v0.22.8 ) replace go.pinniped.dev/generated/1.22/apis => ../apis diff --git a/generated/1.22/client/go.sum b/generated/1.22/client/go.sum index 5f7f4693..a973b114 100644 --- a/generated/1.22/client/go.sum +++ b/generated/1.22/client/go.sum @@ -427,20 +427,20 @@ 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= -k8s.io/api v0.22.6 h1:acjE5ABt0KpsBI9QCtLqaQEPSF94jOtE/LoFxSYasSE= -k8s.io/api v0.22.6/go.mod h1:q1F7IfaNrbi/83ebLy3YFQYLjPSNyunZ/IXQxMmbwCg= -k8s.io/apimachinery v0.22.6 h1:z7vxNRkFX0NToA+8D17kzLZ/T4t+DqwzUlqqbqRepRs= -k8s.io/apimachinery v0.22.6/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= -k8s.io/client-go v0.22.6 h1:ugAXeC312xeGXsn7zTRz+btgtLBnW3qYhtUUpVQL7YE= -k8s.io/client-go v0.22.6/go.mod h1:TffU4AV2idZGeP+g3kdFZP+oHVHWPL1JYFySOALriw0= +k8s.io/api v0.22.8 h1:7Ld6tHuvaYzcQE2axLmomWlhP0fK3vpLfo6fBaNrCIs= +k8s.io/api v0.22.8/go.mod h1:uLlWJNRJ+AYwgAdsNwf0TsD3eByNYW9RlXFmkMdL3yk= +k8s.io/apimachinery v0.22.8 h1:kazMo4/t5ZPI7MwImnCJODZrt1VuwbYBixhTzaNIxsw= +k8s.io/apimachinery v0.22.8/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= +k8s.io/client-go v0.22.8 h1:dWgwPqpWH/DPLWSczA6b61VxFIILe989MXipoE9332s= +k8s.io/client-go v0.22.8/go.mod h1:dOHOy82WOBz0siYHpVyY7FqTIq+iXFXW3+THFk6qErU= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 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-openapi v0.0.0-20211109043538-20434351676c h1:jvamsI1tn9V0S8jicyX82qaFC0H/NKxv2e5mbqsgR80= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/generated/1.23/apis/go.mod b/generated/1.23/apis/go.mod index b728d3d2..a01ea546 100644 --- a/generated/1.23/apis/go.mod +++ b/generated/1.23/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.23/apis go 1.13 require ( - k8s.io/api v0.23.3 - k8s.io/apimachinery v0.23.3 + k8s.io/api v0.23.5 + k8s.io/apimachinery v0.23.5 ) diff --git a/generated/1.23/apis/go.sum b/generated/1.23/apis/go.sum index 3f0ad86b..73deec9f 100644 --- a/generated/1.23/apis/go.sum +++ b/generated/1.23/apis/go.sum @@ -219,10 +219,10 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38= -k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= -k8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk= -k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 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= diff --git a/generated/1.23/client/go.mod b/generated/1.23/client/go.mod index 408cef03..bd82920b 100644 --- a/generated/1.23/client/go.mod +++ b/generated/1.23/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.23/apis v0.0.0 - k8s.io/apimachinery v0.23.3 - k8s.io/client-go v0.23.3 + k8s.io/apimachinery v0.23.5 + k8s.io/client-go v0.23.5 ) replace go.pinniped.dev/generated/1.23/apis => ../apis diff --git a/generated/1.23/client/go.sum b/generated/1.23/client/go.sum index ab2c1158..fbe0a3be 100644 --- a/generated/1.23/client/go.sum +++ b/generated/1.23/client/go.sum @@ -593,12 +593,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh 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= -k8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38= -k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= -k8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk= -k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/client-go v0.23.3 h1:23QYUmCQ/W6hW78xIwm3XqZrrKZM+LWDqW2zfo+szJs= -k8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= +k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 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= diff --git a/hack/lib/kube-versions.txt b/hack/lib/kube-versions.txt index 0d833e3d..62edf1ff 100644 --- a/hack/lib/kube-versions.txt +++ b/hack/lib/kube-versions.txt @@ -1,7 +1,7 @@ -1.23.3 -1.22.6 -1.21.9 -1.20.14 +1.23.5 +1.22.8 +1.21.10 +1.20.15 1.19.15 1.18.18 1.17.16 From 132d2aac729f3f0c8de5759fb034e56918109309 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 19 Apr 2022 11:35:46 -0700 Subject: [PATCH 07/77] add a code comment --- internal/oidc/discovery/discovery_handler.go | 6 ++++-- internal/oidc/discovery/discovery_handler_test.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/oidc/discovery/discovery_handler.go index 8adf2350..f1fb9f82 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/oidc/discovery/discovery_handler.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package discovery provides a handler for the OIDC discovery endpoint. @@ -37,7 +37,9 @@ type Metadata struct { TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` ScopesSupported []string `json:"scopes_supported"` ClaimsSupported []string `json:"claims_supported"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + + // https://datatracker.ietf.org/doc/html/rfc8414#section-2 says, “If omitted, the authorization server does not support PKCE.” + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` // ^^^ Optional ^^^ diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/oidc/discovery/discovery_handler_test.go index 293cdad4..f8d8303f 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/oidc/discovery/discovery_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package discovery From 0b72f7084ccbd0de631fe5d4523fcd99d264fbb2 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 18 Apr 2022 11:46:33 -0700 Subject: [PATCH 08/77] JWTAuthenticator distributed claims resolution honors tls config Kube 1.23 introduced a new field on the OIDC Authenticator which allows us to pass in a client with our own TLS config. See https://github.com/kubernetes/kubernetes/pull/106141. Signed-off-by: Margo Crawford --- .../jwtcachefiller/jwtcachefiller.go | 18 +-- .../jwtcachefiller/jwtcachefiller_test.go | 124 +++++++++++++++--- 2 files changed, 112 insertions(+), 30 deletions(-) diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go index f45cd058..c73a351d 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package jwtcachefiller implements a controller for filling an authncache.Cache with each @@ -17,7 +17,6 @@ import ( "gopkg.in/square/go-jose.v2" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apiserver/pkg/authentication/authenticator" - "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc" "k8s.io/klog/v2" @@ -150,19 +149,11 @@ func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwt // newJWTAuthenticator creates a jwt authenticator from the provided spec. func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) { - rootCAs, caBundle, err := pinnipedauthenticator.CABundle(spec.TLS) + rootCAs, _, err := pinnipedauthenticator.CABundle(spec.TLS) if err != nil { return nil, fmt.Errorf("invalid TLS configuration: %w", err) } - var caContentProvider oidc.CAContentProvider - if len(caBundle) != 0 { - var caContentProviderErr error - caContentProvider, caContentProviderErr = dynamiccertificates.NewStaticCAContent("ignored", caBundle) - if caContentProviderErr != nil { - return nil, caContentProviderErr // impossible since caBundle is validated already - } - } usernameClaim := spec.Claims.Username if usernameClaim == "" { usernameClaim = defaultUsernameClaim @@ -199,7 +190,6 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica if len(providerJSON.JWKSURL) == 0 { return nil, fmt.Errorf("issuer %q does not have jwks_uri set", spec.Issuer) } - oidcAuthenticator, err := oidc.New(oidc.Options{ IssuerURL: spec.Issuer, KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL), @@ -207,9 +197,7 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica UsernameClaim: usernameClaim, GroupsClaim: groupsClaim, SupportedSigningAlgs: defaultSupportedSigningAlgos(), - // this is still needed for distributed claim resolution, meaning this uses a http client that does not honor our TLS config - // TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106141 - CAContentProvider: caContentProvider, + Client: client, }) if err != nil { return nil, fmt.Errorf("could not initialize authenticator: %w", err) diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go index 40de21a8..be6e05c7 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go @@ -58,6 +58,9 @@ func TestController(t *testing.T) { require.NoError(t, err) goodRSASigningAlgo := jose.RS256 + customGroupsClaim := "my-custom-groups-claim" + distributedGroups := []string{"some-distributed-group-1", "some-distributed-group-2"} + mux := http.NewServeMux() server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tlsserver.AssertTLS(t, r, ptls.Default) @@ -87,6 +90,59 @@ func TestController(t *testing.T) { } require.NoError(t, json.NewEncoder(w).Encode(jwks)) })) + // Claims without the subject, to be used distributed claims tests. + // OIDC 1.0 section 5.6.2: + // A sub (subject) Claim SHOULD NOT be returned from the Claims Provider unless its value + // is an identifier for the End-User at the Claims Provider (and not for the OpenID Provider or another party); + // this typically means that a sub Claim SHOULD NOT be provided. + claimsWithoutSubject := jwt.Claims{ + Issuer: server.URL, + Audience: []string{goodAudience}, + Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)), + NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), + } + mux.Handle("/claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Unfortunately we have to set this up pretty early in the test because we can't redeclare + // mux.Handle. This means that we can't return a different groups claim per test; we have to + // return both and predecide which groups are returned. + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: goodECSigningAlgo, Key: goodECSigningKey}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", goodECSigningKeyID), + ) + require.NoError(t, err) + + builder := jwt.Signed(sig).Claims(claimsWithoutSubject) + + builder = builder.Claims(map[string]interface{}{customGroupsClaim: distributedGroups}) + builder = builder.Claims(map[string]interface{}{"groups": distributedGroups}) + + distributedClaimsJwt, err := builder.CompactSerialize() + require.NoError(t, err) + + _, err = w.Write([]byte(distributedClaimsJwt)) + require.NoError(t, err) + })) + mux.Handle("/wrong_claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Unfortunately we have to set this up pretty early in the test because we can't redeclare + // mux.Handle. This means that we can't return a different groups claim per test; we have to + // return both and predecide which groups are returned. + sig, err := jose.NewSigner( + jose.SigningKey{Algorithm: goodECSigningAlgo, Key: goodECSigningKey}, + (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", goodECSigningKeyID), + ) + require.NoError(t, err) + + builder := jwt.Signed(sig).Claims(claimsWithoutSubject) + + builder = builder.Claims(map[string]interface{}{"some-other-claim": distributedGroups}) + + distributedClaimsJwt, err := builder.CompactSerialize() + require.NoError(t, err) + + _, err = w.Write([]byte(distributedClaimsJwt)) + require.NoError(t, err) + })) goodIssuer := server.URL @@ -108,7 +164,7 @@ func TestController(t *testing.T) { Audience: goodAudience, TLS: tlsSpecFromTLSConfig(server.TLS), Claims: auth1alpha1.JWTTokenClaims{ - Groups: "my-custom-groups-claim", + Groups: customGroupsClaim, }, } otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{ @@ -384,6 +440,7 @@ func TestController(t *testing.T) { goodUsername, tt.wantUsernameClaim, tt.wantGroupsClaim, + goodIssuer, ) { test := test t.Run(test.name, func(t *testing.T) { @@ -418,6 +475,7 @@ func TestController(t *testing.T) { &wellKnownClaims, tt.wantGroupsClaim, groups, + test.distributedGroupsClaimURL, tt.wantUsernameClaim, username, ) @@ -447,9 +505,10 @@ func TestController(t *testing.T) { } // isNotInitialized checks if the error is the internally-defined "oidc: authenticator not initialized" error from -// the underlying OIDC authenticator, which is initialized asynchronously. +// the underlying OIDC authenticator or "verifier is not initialized" from verifying distributed claims, +// both of which are initialized asynchronously. func isNotInitialized(err error) bool { - return err != nil && strings.Contains(err.Error(), "authenticator not initialized") + return err != nil && (strings.Contains(err.Error(), "authenticator not initialized") || strings.Contains(err.Error(), "verifier not initialized")) } func testTableForAuthenticateTokenTests( @@ -462,21 +521,24 @@ func testTableForAuthenticateTokenTests( goodUsername string, expectedUsernameClaim string, expectedGroupsClaim string, + issuer string, ) []struct { - name string - jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) - jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) - wantResponse *authenticator.Response - wantAuthenticated bool - wantErrorRegexp string + name string + jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) + jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) + wantResponse *authenticator.Response + wantAuthenticated bool + wantErrorRegexp string + distributedGroupsClaimURL string } { tests := []struct { - name string - jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) - jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) - wantResponse *authenticator.Response - wantAuthenticated bool - wantErrorRegexp string + name string + jwtClaims func(wellKnownClaims *jwt.Claims, groups *interface{}, username *string) + jwtSignature func(key *interface{}, algo *jose.SignatureAlgorithm, kid *string) + wantResponse *authenticator.Response + wantAuthenticated bool + wantErrorRegexp string + distributedGroupsClaimURL string }{ { name: "good token without groups and with EC signature", @@ -514,6 +576,33 @@ func testTableForAuthenticateTokenTests( }, wantAuthenticated: true, }, + { + name: "good token with good distributed groups", + jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) { + }, + distributedGroupsClaimURL: issuer + "/claim_source", + wantResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: goodUsername, + Groups: []string{"some-distributed-group-1", "some-distributed-group-2"}, + }, + }, + wantAuthenticated: true, + }, + { + name: "distributed groups returns a 404", + jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) { + }, + distributedGroupsClaimURL: issuer + "/not_found_claim_source", + wantErrorRegexp: `oidc: could not expand distributed claims: while getting distributed claim "` + expectedGroupsClaim + `": error while getting distributed claim JWT: 404 Not Found`, + }, + { + name: "distributed groups doesn't return the right claim", + jwtClaims: func(claims *jwt.Claims, groups *interface{}, username *string) { + }, + distributedGroupsClaimURL: issuer + "/wrong_claim_source", + wantErrorRegexp: `oidc: could not expand distributed claims: jwt returned by distributed claim endpoint "` + issuer + `/wrong_claim_source" did not contain claim: `, + }, { name: "good token with groups as string", jwtClaims: func(_ *jwt.Claims, groups *interface{}, username *string) { @@ -644,6 +733,7 @@ func createJWT( claims *jwt.Claims, groupsClaim string, groupsValue interface{}, + distributedGroupsClaimURL string, usernameClaim string, usernameValue string, ) string { @@ -659,6 +749,10 @@ func createJWT( if groupsValue != nil { builder = builder.Claims(map[string]interface{}{groupsClaim: groupsValue}) } + if distributedGroupsClaimURL != "" { + builder = builder.Claims(map[string]interface{}{"_claim_names": map[string]string{groupsClaim: "src1"}}) + builder = builder.Claims(map[string]interface{}{"_claim_sources": map[string]interface{}{"src1": map[string]string{"endpoint": distributedGroupsClaimURL}}}) + } if usernameValue != "" { builder = builder.Claims(map[string]interface{}{usernameClaim: usernameValue}) } From 4de8004094529a33a5a1698fd25fef4746e4171d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 19 Apr 2022 12:12:45 -0700 Subject: [PATCH 09/77] Empty commit to trigger CI From cd982655a2d96b9744bae4738c2ef68d6085c1ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Apr 2022 20:33:38 +0000 Subject: [PATCH 10/77] Bump k8s.io/klog/v2 from 2.40.1 to 2.60.1 Bumps [k8s.io/klog/v2](https://github.com/kubernetes/klog) from 2.40.1 to 2.60.1. - [Release notes](https://github.com/kubernetes/klog/releases) - [Changelog](https://github.com/kubernetes/klog/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes/klog/compare/v2.40.1...v2.60.1) --- updated-dependencies: - dependency-name: k8s.io/klog/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 47a6be97..b83ff056 100644 --- a/go.mod +++ b/go.mod @@ -78,7 +78,7 @@ require ( k8s.io/client-go v0.23.5 k8s.io/component-base v0.23.5 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 - k8s.io/klog/v2 v2.40.1 + k8s.io/klog/v2 v2.60.1 k8s.io/kube-aggregator v0.23.5 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/yaml v1.3.0 diff --git a/go.sum b/go.sum index ee8a7c9c..b34888e9 100644 --- a/go.sum +++ b/go.sum @@ -2077,8 +2077,8 @@ k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/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.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/klog/v2 v2.40.1 h1:P4RRucWk/lFOlDdkAr3mc7iWFkgKrZY9qZMAgek06S4= -k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-aggregator v0.23.5 h1:UZ+qE3hGo6DcgKySf27Jg7d3X9/6JQkVLUiHZAoAfCY= k8s.io/kube-aggregator v0.23.5/go.mod h1:3ynYx07Co6dzjpKPgipM+1/Mt2Jcm7dY++cRlKLr5s8= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= From 46e61bdea9110daa3979b0a4ead8569ecd455688 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 20 Apr 2022 10:56:21 -0700 Subject: [PATCH 11/77] Update 2022-04-15-fips-and-more.md Update release date --- site/content/posts/2022-04-15-fips-and-more.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/content/posts/2022-04-15-fips-and-more.md b/site/content/posts/2022-04-15-fips-and-more.md index ac1c5110..3ed252a3 100644 --- a/site/content/posts/2022-04-15-fips-and-more.md +++ b/site/content/posts/2022-04-15-fips-and-more.md @@ -1,10 +1,10 @@ --- title: "Pinniped v0.16.0: With Build-Your-Own FIPS Binaries, Workspace ONE IDP configuration, and Supervisor HTTP listener changes" slug: fips-and-more -date: 2022-04-14 +date: 2022-04-20 author: Anjali Telang image: https://images.unsplash.com/photo-1618075254478-850bc1729c17?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2274&q=80 -excerpt: "With the release of v0.16.0 , You can now build your own Pinniped binaries with FIPS compliant BoringCrypto, HTTPS will be the default for our public facing Supervisor listener ports, and we provide you with documentation to configure Workspace ONE Access as an OIDC Identity Provider" +excerpt: "You can now build your own Pinniped binaries with FIPS compliant BoringCrypto, HTTPS will be the default for our public facing Supervisor listener ports, and we provide you with documentation to configure Workspace ONE Access as an OIDC Identity Provider" tags: ['Margo Crawford','Ryan Richard', 'Mo Khan', 'Anjali Telang', 'release'] --- From 4071b48f01b983b627214ad95c11810eb97efabe Mon Sep 17 00:00:00 2001 From: Pinny Date: Wed, 20 Apr 2022 18:52:59 +0000 Subject: [PATCH 12/77] Updated versions in docs for v0.16.0 release --- site/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/config.yaml b/site/config.yaml index b0a4baee..24b01b75 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -7,7 +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.15.0 + latest_version: v0.16.0 latest_codegen_version: 1.23 pygmentsCodefences: true pygmentsStyle: "pygments" From 444cf111d08610e0ba26c6c7d3d3396fcccf08cc Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 20 Apr 2022 16:17:49 -0700 Subject: [PATCH 13/77] Add more detail about how the flow should work Signed-off-by: Margo Crawford --- proposals/1113_ldap-ad-web-ui/README.md | 41 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 69baaa34..70d701ed 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -66,11 +66,46 @@ custom Username/Password headers, they should be rejected. The discovery metadata for LDAP/AD IDPS should indicate that they support a flow of `browser_authcode`. -The pinniped cli should default to using the cli-based password flow, but when a tty is unavailable, -it will open a browser to log in +The state param should be augmented to include the IDP type as well as the IDP name. The type +should be included in `UpstreamStateParamData` so that later when we get it back in the callback +request we can tell which IDP it is referring to. This will require an update to +`UpstreamStataParamData.FormatVersion`, which would mean that logins in progress at the time of +upgrade would fail. + +The pinniped cli should default to using the cli-based password flow, but when the `--upstream-identity-provider-flow`, +flag specifies `browser_authcode`, it will open a browser to log in instead of prompting for username and password. Some users (for example, IDE plugins for kubernetes) may wish to authenticate using the pinniped cli but without access to a terminal. +Here is how the login flow might work: +1. The supervisor receives an authorization request. + 1. If the client_id param is not "pinniped-cli", and it includes username and password via the custom headers, reject the request. + 2. If the request does not include the custom username/password headers, assume we want to use the webpage login. + 3. Some day we may want to allow specifying the idp name and type as request parameters, but + for now we do not have multiple idps. + 4. Encode the request parameters into a state param like is done today for the `OIDCIdentityProvider`. + In addition to the values encoded today (auth params, upstream IDP name, nonce, csrf token and pkce), + encode the upstream IDP type. + 5. Set a CSRF cookie on the response like what we do for OIDC today. + 6. Return a redirect to the LDAP web url. This should take the form `/login` +2. The client receives the redirect and follows it to `/login` +3. The supervisor receives the GET request to `/login` and renders a simple login form with the Pinniped +logo and the IDP name. + 1. The submission should be POST `/login`. + 2. The state param’s value is written into a hidden form input, properly escaped. + 3. Username and password form inputs are shown. +4. The supervisor receives the POST request. + 1. Decode your state form param to reconstitute the original authorization request params + (the client’s nonce and PKCE, requested scopes, etc) and also compare the incoming CSRF cookie to the value + from the state param. This code would be identical to what we do in the upstream OIDC callback endpoint today. + If the decoded state param’s timestamp is too old, it might be prudent to reject the request. + 2. Using the idp name/type from the state param, look up the IDP, bind to it, verify the username/password and + get the users downstream username and groups. + 3. If the login succeeds, mint an authcode and store the session as a secret the same way as we do on the + callback endpoint today. Redirect to the client’s redirect URI with the new authcode. + 4. If the login fails, respond with an error so the login page can render an error message. Allow the user to retry + login the same way we do with the CLI today (we leave brute force protection to the IDP) + #### Upgrades This change is backwards compatible. Users would see no changes unless they decided to register @@ -134,8 +169,6 @@ config because Dex does not have an equivalent. * Currently we have little validation on branding requirements. Is specifying the idp name enough for users to understand how to log in? * How many users will be blocked on using this feature until they can have a company name and logo on the login page? -* Should we allow admins or users to decide to use the web ui with the pinniped cli, or is it sufficient for us to - determine it based on presence/absence of tty? ## Implementation Plan While this work is intended to supplement the dynamic client work, parts of it From cab9ac8368c4250db77d6437c83671d9f0862f48 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 21 Apr 2022 09:17:24 -0700 Subject: [PATCH 14/77] bump kube deps from v0.23.5 to v0.23.6 --- go.mod | 14 +++++++------- go.sum | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index b83ff056..d56bdbf8 100644 --- a/go.mod +++ b/go.mod @@ -71,15 +71,15 @@ require ( golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/text v0.3.7 gopkg.in/square/go-jose.v2 v2.6.0 - k8s.io/api v0.23.5 - k8s.io/apiextensions-apiserver v0.23.5 - k8s.io/apimachinery v0.23.5 - k8s.io/apiserver v0.23.5 - k8s.io/client-go v0.23.5 - k8s.io/component-base v0.23.5 + k8s.io/api v0.23.6 + k8s.io/apiextensions-apiserver v0.23.6 + k8s.io/apimachinery v0.23.6 + k8s.io/apiserver v0.23.6 + k8s.io/client-go v0.23.6 + k8s.io/component-base v0.23.6 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 k8s.io/klog/v2 v2.60.1 - k8s.io/kube-aggregator v0.23.5 + k8s.io/kube-aggregator v0.23.6 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/yaml v1.3.0 ) diff --git a/go.sum b/go.sum index b34888e9..9dda3018 100644 --- a/go.sum +++ b/go.sum @@ -2058,19 +2058,19 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= -k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= -k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= -k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= -k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= -k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apiserver v0.23.5 h1:2Ly8oUjz5cnZRn1YwYr+aFgDZzUmEVL9RscXbnIeDSE= -k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= -k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= -k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= -k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= -k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= +k8s.io/api v0.23.6 h1:yOK34wbYECH4RsJbQ9sfkFK3O7f/DUHRlzFehkqZyVw= +k8s.io/api v0.23.6/go.mod h1:1kFaYxGCFHYp3qd6a85DAj/yW8aVD6XLZMqJclkoi9g= +k8s.io/apiextensions-apiserver v0.23.6 h1:v58cQ6Z0/GK1IXYr+oW0fnYl52o9LTY0WgoWvI8uv5Q= +k8s.io/apiextensions-apiserver v0.23.6/go.mod h1:YVh17Mphv183THQJA5spNFp9XfoidFyL3WoDgZxQIZU= +k8s.io/apimachinery v0.23.6 h1:RH1UweWJkWNTlFx0D8uxOpaU1tjIOvVVWV/bu5b3/NQ= +k8s.io/apimachinery v0.23.6/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apiserver v0.23.6 h1:p94LiXcsSnpSDIl4cv98liBuFKcaygSCNopFNfMg/Ac= +k8s.io/apiserver v0.23.6/go.mod h1:5PU32F82tfErXPmf7FXhd/UcuLfh97tGepjKUgJ2atg= +k8s.io/client-go v0.23.6 h1:7h4SctDVQAQbkHQnR4Kzi7EyUyvla5G1pFWf4+Od7hQ= +k8s.io/client-go v0.23.6/go.mod h1:Umt5icFOMLV/+qbtZ3PR0D+JA6lvvb3syzodv4irpK4= +k8s.io/code-generator v0.23.6/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/component-base v0.23.6 h1:8dhVZ4VrRcNdV2EGjl8tj8YOHwX6ysgCGMJ2Oyy0NW8= +k8s.io/component-base v0.23.6/go.mod h1:FGMPeMrjYu0UZBSAFcfloVDplj9IvU+uRMTOdE23Fj0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -2079,8 +2079,8 @@ k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.23.5 h1:UZ+qE3hGo6DcgKySf27Jg7d3X9/6JQkVLUiHZAoAfCY= -k8s.io/kube-aggregator v0.23.5/go.mod h1:3ynYx07Co6dzjpKPgipM+1/Mt2Jcm7dY++cRlKLr5s8= +k8s.io/kube-aggregator v0.23.6 h1:/p1FvmG3je8kSv+i6uJoK+LkViOgu1vhV+BpGgibdCk= +k8s.io/kube-aggregator v0.23.6/go.mod h1:cubFdoSJRMEN+ilg1ErhNIoplJwyYbmgn3bUlen8KjA= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf h1:M9XBsiMslw2lb2ZzglC0TOkBPK5NQi0/noUrdnoFwUg= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= From 973c3102bb550929ca77c00bfc0bc88bc373eba8 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 21 Apr 2022 14:50:48 -0700 Subject: [PATCH 15/77] add audit logging proposal --- proposals/1141_audit-logging/README.md | 323 +++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 proposals/1141_audit-logging/README.md diff --git a/proposals/1141_audit-logging/README.md b/proposals/1141_audit-logging/README.md new file mode 100644 index 00000000..d6ae0341 --- /dev/null +++ b/proposals/1141_audit-logging/README.md @@ -0,0 +1,323 @@ +--- +title: "Audit Logging" +authors: [ "@cfryanr" ] +status: "in-review" +sponsor: [ ] +approval_date: "" +--- + +*Disclaimer*: Proposals are point-in-time designs and decisions. Once approved and implemented, they become historical +documents. If you are reading an old proposal, please be aware that the features described herein might have continued +to evolve since. + +# Audit Logging + +## Problem Statement + +Audit logging is a requirement from most compliance standards (e.g. FedRAMP, PCI-DSS). The Pinniped Supervisor and +Concierge components should provide audit logs to help users meet these compliance requirements. + +The Kubernetes API server already supports +rich [audit logging features](https://kubernetes.io/docs/tasks/debug-application-cluster/audit/) which are implemented +by vendors of Kubernetes distributions. The Pinniped audit logs are meant to augment, not replace, the Kubernetes audit +logs. + +### How Pinniped Works Today (as of version v0.16.0) + +The Pinniped Supervisor and Concierge components are Kubernetes Deployments. Today, each Pod has a single container, +which is the Supervisor or Concierge app. Kubernetes captures the stdout of the app into the Pod logs. + +Today, the Pinniped Supervisor and Concierge log many interesting events to their Pod logs. These logs are meant +primarily to help an admin user debug problems with their Pinniped configuration or with their cluster. The Supervisor +and Concierge each offer an install-time configuration option to turn up the verbosity of these Pod logs. + +However, these logs are not meant to be audit logs. They generally focus on logging problems, not on logging successes. +They try to avoid logging anything that might be confidential or PII (personally identifiable information). Since email +addresses might be considered PII, these logs generally avoid including usernames at the default log level, since +usernames could be email addresses in some configurations. Logging the identity of actors (usernames) are a key aspect +of audit logs. + +## Terminology / Concepts + +None. + +## Proposal + +The goal of an audit log is to log events that could be helpful in a forensic investigation of past usage, including the +actor (the username) and the actions that were taken on the system. + +### Goals and Non-goals + +Goals + +- Auditing events relating to upstream identity provider (IDP) authentication, refresh, and sessions. +- Auditing events relating to minting and validating cluster credentials. +- Enabling auditors to easily stitch together authentication events into an audit trail. +- Provide consistent data across auditable events. +- Provide the ability to enable and disable auditing. +- Provide the ability to route audit logs to a separate destination from the rest of Pinniped’s logs. + +Non-goals + +- Enabling auditing in the impersonation proxy. If needed, this will be handled in a separate feature. +- Providing the ability to filter or choose which audit events to capture. +- Auditing the management of CRs (e.g. OIDCIdentityProvider). These events are captured by the API server audit logs. + +### Specification / How it Solves the Use Cases + +This proposal recommends following the recommendation of the Kubernetes docs to create a separate Pod container log. +This new container log will contain the audit logs (and only the audit logs). + +#### API Changes + +##### Configuration Options + +There will be very few user-facing configuration options for audit logging in the first version of the feature. If later +found to be needed, more configuration could be added in future versions. + +This proposal recommends adding a single on/off install-time configuration option for disabling audit logs. By default, +audit logs will be enabled. An admin user who is concerned about logging identities, for example because usernames may +be considered PII, may disable audit logging. + +Like other install-time configuration options, this option would appear in the values.yaml file of the Supervisor and +Concierge deployment directories. The selected value would be rendered into the "static" ConfigMap, and read by the +Supervisor or Concierge app's Golang code at Pod startup time. + +##### Event Data + +Deciding every specific audit event is an implementation detail beyond the scope of this proposal. + +Generally, the following data should be included with every audit event, whenever possible: + +- What type of event occurred (e.g. login) +- Outcomes of event (succeed or fail) +- When the event occurred +- Where the event occurred (Kubernetes Pod logs automatically include the ID of the Pod, which should be sufficient) +- Source of the event (e.g. requester IP address) +- The identity of individuals or subjects associated with the event (who initiated, who participated. etc.) +- Details involving any objects accessed + +The Supervisor's audit logs would include events such as: + +- Upstream logins for all IdP types (started, succeeded, failed) +- Upstream refresh for all IdP types (succeeded, failed) +- Upstream group refresh for all IdP types (succeeded, failed) +- Downstream login (started, succeeded, failed) +- Downstream token exchange (succeeded, failed) +- Session expired +- Maybe: The equivalent of access log events for all Supervisor endpoints, since there is no other component providing + access logs. This would include logging things like calls to the Supervisor's OIDC well-known discovery endpoint. + These logs could help an investigator determine more about the usage pattern of a suspicious client. +- Maybe: Newly authenticated user is associated with “admin” RBAC. Note that the Supervisor is not directly aware of + RBAC, so determining this would require otherwise unnecessary calls to the Kubernetes API server, which would degrade + the performance of the Supervisor. It's also not clear what would constitute "admin" level access, since RBAC is + configurable at a very fine-grained level. On the other hand, the Supervisor is directly aware of the user's group + memberships, which could be logged. + +The Concierge's audit logs would include events such as: + +- Token credential request (succeeded, failed, maybe maps to admin RBAC). While already captured by the API server audit + logs, those should likely be set to metadata. Duplicating the event allows for more controlled capture & management of + data. +- WhoAmI Request. While already captured by the API server audit logs, duplicating the event allows for more controlled + capture & management of data. + +Other events may be useful to auditors and may be included in the audit logs, such as: + +- Application startup with version information +- Graceful application shutdown + +##### Audit Logs as Separate Log Files + +The Concierge and Supervisor apps could each send audit logs to separate files on disk in JSON format. The performance +impact of logging to a file should be acceptable thanks to file buffering, but this assumption should be tested. Note +that this approach would not guarantee that the log statement is flushed to the file before the action is performed, +because then we would lose the benefit of buffering. It would be "best effort" to the file, e.g. the process crashing +might lose a few lines of logs. A normal pod shutdown should be able to flush the file without any loss. + +[A new streaming sidecar container](https://kubernetes.io/docs/concepts/cluster-administration/logging/#sidecar-container-with-logging-agent) +will be added to both the Concierge and Supervisor apps Deployments' Pods. These containers will tail those audit logs +to stdout, thus effectively moving those log lines from files on the Pod to Kubernetes container logs. Those sidecar +container images can be minimal with just enough in the image to support the unix `tail` command (or similar Go binary, +such as [hpcloud/tail](https://github.com/hpcloud/tail)). + +Kubernetes will take care of concerns such as log rotation for the container logs. For the files on the Pod's disk +output by the Supervisor and Concierge apps, we should research whether Pinniped should have code to avoid allowing +those files from growing too large. Old lines can be discarded since the sidecar container should have already streamed +them. + +Container logs in JSON format are easy for node-level logging agents, e.g. fluentbit, to ingest/annotate/parse/filter +and send to numerous sink destinations. These containers could still run when audit logs are disabled by the admin, but +would produce no log lines in that case. + +##### Parsing, Filtering, and Sending Audit Logs to an External Destination + +Many users will use the popular [fluentbit](https://fluentbit.io) project to filter and extract Pod logs from their +cluster. This project implements +a [node-level log agent](https://kubernetes.io/docs/concepts/cluster-administration/logging/#using-a-node-logging-agent) +which understands the Kubernetes directory and file layout for Pod logs. It also has a feature to further enrich the +logs +by [automatically adding more information about the source Pod](https://docs.fluentbit.io/manual/pipeline/filters/kubernetes) +to each event (line) in the log. It supports many configurable options +for [parsing](https://docs.fluentbit.io/manual/pipeline/parsers), +[filtering](https://docs.fluentbit.io/manual/pipeline/filters), and sending logs +to [many destinations](https://docs.fluentbit.io/manual/pipeline/outputs). + +By putting the Supervisor and Concierge audit logs into their own Pod logs, Pinniped will be compatible with any +existing node-level agent software which can extract logs from a Kubernetes cluster. This allows the Pinniped code to +focus on generating the logs as JSON, without worrying about providing any configuration options for filtering or +sending to various destinations. + +##### Audit Log JSON Format + +Each line of audit log will represent an event. Each line will be a complete JSON object, +i.e. `{"key1":"value1","key2":"value2"}`. + +Some, but not all, events will be the result of a user making an API request to an endpoint. One API request from a user +may cause more than one event to be logged. If possible, unique ID will be determined for each incoming request, and +will be included in all events caused by that request. + +Where possible, the top-level keys of the JSON object will use standardized names. Other top-level keys specific to that +action type may be added. All keys should be included in documentation for the audit log feature. + +Every event should include these keys: + +- `time`: the timestamp of the event +- `event`: the event type, which is a brief description of what happened, with no string interpolation, so it will + always be the same for a given event type (e.g. `upstream refresh succeeded`) +- `v`: a number specifying the format version of the event type, starting with `1`, to give us flexibility to make + breaking changes to the format of an event type in future releases (e.g. change the name of the JSON keys, or change + the data type of the value of an existing key) + +Depending on the event type, an event might include other keys, such as: + +- `msg`: a freeform warning or error message meant to be read by a human (e.g. the error message that was returned by an + upstream IDP during a failed login attempt) +- `requestID`: a unique ID for the request, if the event is related to an API request +- `requestPath`: the path of the endpoint, if the event is related to an API request +- `requestorIP`: the client's IP, if the event is related to an API request +- `user`: the username of the user performing the action, if there is one +- `groups`: the group memberships of the user performing the action, if the action is related determining or changing + their group memberships + +The details of these additional keys will be worked out as the details of the specific events are being worked out, +during implementation of this proposal. + +##### Audit Log Timestamps + +The date format used in the audit logs should be something which can be easily parsed by fluentbit, to make it easy for +users to configure fluentbit. We could easily document this to provide instructions on how to configure a custom +fluentbit parser for Pinniped audit logs. We should probably +avoid [fluentbit's default json parser's](https://github.com/fluent/fluent-bit/blob/845b6ae8576077fd512dbe64fb8e16ff4b15abdb/conf/parsers.conf#L35-L39) +date format, which assumes dates will be in an ugly format and also lacks sub-second precision +(e.g. `08/Apr/2022:19:24:01 +0000`). + +fluentbit uses [strptime](https://linux.die.net/man/3/strptime) +with [an extension for fractional seconds](https://docs.fluentbit.io/manual/pipeline/parsers/configuring-parser#time-resolution-and-fractional-seconds) +to parse timestamps. + +It would be desirable for a timestamp to: + +1. Be human-readable (e.g. not seconds since an epoch) +2. Be easily parsable by log parsers, especially fluentbit +3. Be expressed in UTC time +4. Use at least millisecond precision +5. Use the consistent JSON key name `time` + +[Syslog's RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.3) defines a timestamp format which meets +the above goals. An example timestamp in this format is `2003-10-11T22:14:15.003` which is represents UTC time on +October 11, 2003 at 10:14:15 pm, 3 milliseconds into the next second. + +Given this timestamp format, the following fluentbit configuration could be used to parse Pinniped's audit logs. + +``` + [PARSER] + Name json + Format json + Time_Key time + Time_Format %Y-%m-%dT%H:%M:%S.%L +``` + +#### Upgrades + +Since audit logs will be output to a new location, there are not any backward compatibility concerns for them in the +first release. + +Adding a second container to the Pods in generally not noticeable by a user, but may have some impact on existing +installations in some rare cases, so it should be explained in the release notes. For example, a GKE Ingress will, by +default, read the Pod's container definition to try to guess the health check endpoint for the backend Service of the +Ingress. When there is only one container, it will try to guess, but where there is more than one container it will give +up on guessing and instead expect the user to configure the health checks. So upgrading could break the health checks of +a GKE Ingress, if no health checks were configured. + +#### Tests + +Audit logging will be a user-facing feature, and the format of the logs should be considered a kind of documented API. +Unnecessary changes to the format should be avoided after the first release. Therefore, all audit log events should be +covered by unit tests. + +This implies that it may be desirable for the implementation to involve passing around a pointer to some interface to +all code which needs to add events to the audit log. Such an implementation would make the audit logs more testable. A +production code implementation of the interface should take care of common concerns, such as adding the timestamp, +deciding required key names, and formatting the output as JSON. A test implementation of the interface could handle +those common concerns differently to make testing easier. + +#### New Dependencies + +- We might want to consider using a library like [zap](https://github.com/uber-go/zap) to aid in implementation, but + that is already an indirect dependency of Pinniped. +- The new streaming sidecar container will need a container image. Using the existing pinniped-server container image + seems desirable. It is a distroless image, which is good for security. And it is the only image that we currently ship + in Pinniped releases. One option to make this happen would be to implement the tail command in Go, but any binary that + can work in a distroless image should be okay. We should avoid adding linux standard libraries to the container image, + so the binary should be statically linked with no external dependencies. The binary should support the same OS and + architecture that our existing Go binary supports. + +#### Performance Considerations + +By using buffered output to write to the audit log files, there should not be any meaningful performance impact. This +assumption should be tested. + +#### Observability Considerations + +Auditing will improve operator observability, as described in the other sections of this document. + +#### Security Considerations + +The audit logs will be Pod container logs, so the contents of the logs will be protected by Kubernetes like any Pod +container logs. + +#### Usability Considerations + +By using Pod container logs, the user will have many options to manage these logs. + +#### Documentation Considerations + +The supported audit event types, and they JSON keys output for each event type, should be documented. Users should be +able to build their own parsers for these events based on the documentation. + +If the production code implementation of the audit interface used Golang constants for all allowed JSON key names and +event type names, and otherwise enforced certain standards, then it may be possible to auto-generate (or nearly +auto-generate) the documentation for the audit event types. + +### Other Approaches Considered + +None yet. + +## Open Questions + +- Should we output events that can function similar to access logs for the Supervisor endoints? +- Should we try to somehow detect that a user is "root-like"? + +## Answered Questions + +None yet. + +## Implementation Plan + +The maintainers will implement these features. It might fit into one PR. + +## Implementation PRs + +*This section is a placeholder to list the PRs that implement this proposal. This section should be left empty until +after the proposal is approved. After implementation, the proposal can be updated to list related implementation PRs.* From 694e4d6df6885106966bac80edbca04d1a0053da Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 20 Apr 2022 14:58:09 -0700 Subject: [PATCH 16/77] Advertise browser_authcode flow in ldap idp discovery To keep this backwards compatible, this PR changes how the cli deals with ambiguous flows. Previously, if there was more than one flow advertised, the cli would require users to set the flag --upstream-identity-provider-flow. Now it chooses the first one in the list. Signed-off-by: Margo Crawford --- cmd/pinniped/cmd/kubeconfig.go | 15 +++--- cmd/pinniped/cmd/kubeconfig_test.go | 48 +++++++++++++++++-- .../idpdiscovery/idp_discovery_handler.go | 6 +-- .../idp_discovery_handler_test.go | 20 ++++---- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index f10fcd43..74280ec5 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -233,7 +233,7 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f // When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input. Maybe they know something // that we can't know, like the name of an IDP that they are going to define in the future. if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "") { - if err := discoverSupervisorUpstreamIDP(ctx, &flags); err != nil { + if err := discoverSupervisorUpstreamIDP(ctx, &flags, deps.log); err != nil { return err } } @@ -726,7 +726,7 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool return false } -func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error { +func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams, log logr.Logger) error { httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle) if err != nil { return err @@ -758,7 +758,7 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigPara return err } - selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow) + selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow, log) if err != nil { return err } @@ -898,7 +898,7 @@ func selectUpstreamIDPNameAndType(pinnipedIDPs []idpdiscoveryv1alpha1.PinnipedID } } -func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string) (idpdiscoveryv1alpha1.IDPFlow, error) { +func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string, log logr.Logger) (idpdiscoveryv1alpha1.IDPFlow, error) { switch { case len(discoveredIDPFlows) == 0: // No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed. @@ -922,10 +922,7 @@ func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, se return discoveredIDPFlows[0], nil default: // The user did not specify a flow, and more than one was found. - return "", fmt.Errorf( - "multiple client flows for Supervisor upstream identity provider %q of type %q were found, "+ - "so the --upstream-identity-provider-flow flag must be specified. "+ - "Found these flows: %v", - selectedIDPName, selectedIDPType, discoveredIDPFlows) + log.Info("multiple client flows found, selecting first value as default: "+discoveredIDPFlows[0].String(), "idpName", selectedIDPName, "idpType", selectedIDPType) + return discoveredIDPFlows[0], nil } } diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index e5c760aa..e5c27797 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -1261,13 +1261,51 @@ func TestGetKubeconfig(t *testing.T) { oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ - {"name": "some-oidc-idp", "type": "oidc", "flows": ["flow1", "flow2"]} + {"name": "some-ldap-idp", "type": "ldap", "flows": ["cli_password", "flow2"]} ] }`), - wantError: true, - wantStderr: func(issuerCABundle string, issuerURL string) string { - return `Error: multiple client flows for Supervisor upstream identity provider "some-oidc-idp" of type "oidc" were found, so the --upstream-identity-provider-flow flag must be specified.` + - ` Found these flows: [flow1 flow2]` + "\n" + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + server: https://fake-server-url-value + name: kind-cluster-pinniped + contexts: + - context: + cluster: kind-cluster-pinniped + user: kind-user-pinniped + name: kind-context-pinniped + current-context: kind-context-pinniped + kind: Config + preferences: {} + users: + - name: kind-user-pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --upstream-identity-provider-name=some-ldap-idp + - --upstream-identity-provider-type=ldap + - --upstream-identity-provider-flow=cli_password + command: '.../path/to/pinniped' + env: [] + installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli + for more details + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + wantLogs: func(_ string, _ string) []string { + return []string{"\"level\"=0 \"msg\"=\"multiple client flows found, selecting first value as default: cli_password\" \"idpName\"=\"some-ldap-idp\" \"idpType\"=\"ldap\""} }, }, { diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler.go b/internal/oidc/idpdiscovery/idp_discovery_handler.go index 8949502c..66a974c9 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler.go @@ -1,4 +1,4 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package idpdiscovery provides a handler for the upstream IDP discovery endpoint. @@ -44,14 +44,14 @@ func responseAsJSON(upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte, r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ Name: provider.GetName(), Type: v1alpha1.IDPTypeLDAP, - Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword}, + Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode}, }) } for _, provider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ Name: provider.GetName(), Type: v1alpha1.IDPTypeActiveDirectory, - Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword}, + Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode}, }) } for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go index f5e601bd..b33ab2d8 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package idpdiscovery @@ -37,22 +37,22 @@ func TestIDPDiscovery(t *testing.T) { wantContentType: "application/json", wantFirstResponseBodyJSON: here.Doc(`{ "pinniped_identity_providers": [ - {"name": "a-some-ldap-idp", "type": "ldap", "flows": ["cli_password"]}, + {"name": "a-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "a-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]}, - {"name": "x-some-idp", "type": "ldap", "flows": ["cli_password"]}, + {"name": "x-some-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "x-some-idp", "type": "oidc", "flows": ["browser_authcode"]}, - {"name": "y-some-ad-idp", "type": "activedirectory", "flows": ["cli_password"]}, - {"name": "z-some-ad-idp", "type": "activedirectory", "flows": ["cli_password"]}, - {"name": "z-some-ldap-idp", "type": "ldap", "flows": ["cli_password"]}, + {"name": "y-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, + {"name": "z-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, + {"name": "z-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "z-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode", "cli_password"]} ] }`), wantSecondResponseBodyJSON: here.Doc(`{ "pinniped_identity_providers": [ - {"name": "some-other-ad-idp-1", "type": "activedirectory", "flows": ["cli_password"]}, - {"name": "some-other-ad-idp-2", "type": "activedirectory", "flows": ["cli_password"]}, - {"name": "some-other-ldap-idp-1", "type": "ldap", "flows": ["cli_password"]}, - {"name": "some-other-ldap-idp-2", "type": "ldap", "flows": ["cli_password"]}, + {"name": "some-other-ad-idp-1", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, + {"name": "some-other-ad-idp-2", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, + {"name": "some-other-ldap-idp-1", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, + {"name": "some-other-ldap-idp-2", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "some-other-oidc-idp-1", "type": "oidc", "flows": ["browser_authcode", "cli_password"]}, {"name": "some-other-oidc-idp-2", "type": "oidc", "flows": ["browser_authcode"]} ] From 8832362b94b4cf016c64bd6fbe6c1861726c13d3 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 25 Apr 2022 16:41:55 -0700 Subject: [PATCH 17/77] WIP: Add login handler for LDAP/AD web login Also change state param to include IDP type --- internal/oidc/login/login_handler.go | 23 +++++++++++++++++++++++ internal/oidc/oidc.go | 6 ++++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 internal/oidc/login/login_handler.go diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go new file mode 100644 index 00000000..10727b3c --- /dev/null +++ b/internal/oidc/login/login_handler.go @@ -0,0 +1,23 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" +) + +// NewHandler returns an http.Handler that serves the login endpoint for IDPs that +// don't have their own Web UI. +func NewHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) + return + } + _, err := w.Write([]byte("

hello world

")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 6c3c1918..9467eb22 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package oidc contains common OIDC functionality needed by Pinniped. @@ -26,13 +26,14 @@ const ( CallbackEndpointPath = "/callback" JWKSEndpointPath = "/jwks.json" PinnipedIDPsPathV1Alpha1 = "/v1alpha1/pinniped_identity_providers" + PinnipedLoginPath = "/login" ) const ( // Just in case we need to make a breaking change to the format of the upstream state param, // we are including a format version number. This gives the opportunity for a future version of Pinniped // to have the consumer of this format decide to reject versions that it doesn't understand. - UpstreamStateParamFormatVersion = "1" + UpstreamStateParamFormatVersion = "2" // The `name` passed to the encoder for encoding the upstream state param value. This name is short // because it will be encoded into the upstream state param value and we're trying to keep that small. @@ -93,6 +94,7 @@ type Codec interface { type UpstreamStateParamData struct { AuthParams string `json:"p"` UpstreamName string `json:"u"` + UpstreamType string `json:"t"` Nonce nonce.Nonce `json:"n"` CSRFToken csrftoken.CSRFToken `json:"c"` PKCECode pkce.Code `json:"k"` From eb1d3812ece58591b5a6a5728af7eea40fade5a9 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 26 Apr 2022 12:51:56 -0700 Subject: [PATCH 18/77] Update authorization endpoint to redirect to new login page Also fix some test failures on the callback handler, register the new login handler in manager.go and add a (half baked) integration test Signed-off-by: Margo Crawford --- internal/oidc/auth/auth_handler.go | 115 ++++++++++++++++- internal/oidc/auth/auth_handler_test.go | 112 +++++++++++++---- .../oidc/callback/callback_handler_test.go | 3 +- internal/oidc/provider/manager/manager.go | 6 +- .../testutil/oidctestutil/oidctestutil.go | 1 + test/integration/e2e_test.go | 116 ++++++++++++++++++ 6 files changed, 323 insertions(+), 30 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 4f281ad8..5b4d0bcb 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -7,6 +7,7 @@ package auth import ( "fmt" "net/http" + "net/url" "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" @@ -75,15 +76,33 @@ func NewHandler( cookieCodec, ) } - return handleAuthRequestForLDAPUpstream(r, w, - oauthHelperWithStorage, + + // we know it's an AD/LDAP upstream. + if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 || len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 { + // The client set a username header, so they are trying to log in with a username/password. + return handleAuthRequestForLDAPUpstreamCLIFlow(r, w, + oauthHelperWithStorage, + ldapUpstream, + idpType, + ) + } + return handleAuthRequestForLDAPUpstreamBrowserFlow( + r, + w, + oauthHelperWithoutStorage, + generateCSRF, + generateNonce, + generatePKCE, ldapUpstream, idpType, + downstreamIssuer, + upstreamStateEncoder, + cookieCodec, ) })) } -func handleAuthRequestForLDAPUpstream( +func handleAuthRequestForLDAPUpstreamCLIFlow( r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, @@ -138,6 +157,93 @@ func handleAuthRequestForLDAPUpstream( oauthHelper, authorizeRequester, subject, username, groups, customSessionData) } +func handleAuthRequestForLDAPUpstreamBrowserFlow( + r *http.Request, + w http.ResponseWriter, + oauthHelper fosite.OAuth2Provider, + generateCSRF func() (csrftoken.CSRFToken, error), + generateNonce func() (nonce.Nonce, error), + generatePKCE func() (pkce.Code, error), + ldapUpstream provider.UpstreamLDAPIdentityProviderI, + idpType psession.ProviderType, + downstreamIssuer string, + upstreamStateEncoder oidc.Encoder, + cookieCodec oidc.Codec, +) error { + authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) + if !created { + return nil + } + + now := time.Now() + _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. + Subject: "none", + AuthTime: now, + RequestedAt: now, + }, + }, + }) + if err != nil { + return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false) + } + + csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) + if err != nil { + plog.Error("authorize generate error", err) + return err + } + csrfFromCookie := readCSRFCookie(r, cookieCodec) + if csrfFromCookie != "" { + csrfValue = csrfFromCookie + } + + encodedStateParamValue, err := upstreamStateParam( + authorizeRequester, + ldapUpstream.GetName(), + string(idpType), + nonceValue, + csrfValue, + pkceValue, + upstreamStateEncoder, + ) + if err != nil { + plog.Error("authorize upstream state param error", err) + return err + } + + promptParam := r.Form.Get(promptParamName) + if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { + return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) + } + + if csrfFromCookie == "" { + // We did not receive an incoming CSRF cookie, so write a new one. + err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) + if err != nil { + plog.Error("error setting CSRF cookie", err) + return err + } + } + + loginURL, err := url.Parse(downstreamIssuer + "/login") + if err != nil { + return err + } + q := loginURL.Query() + q.Set("state", encodedStateParamValue) + loginURL.RawQuery = q.Encode() + + http.Redirect(w, r, + loginURL.String(), + http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 + ) + + return nil +} + func handleAuthRequestForOIDCUpstreamPasswordGrant( r *http.Request, w http.ResponseWriter, @@ -246,6 +352,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( encodedStateParamValue, err := upstreamStateParam( authorizeRequester, oidcUpstream.GetName(), + string(psession.ProviderTypeOIDC), nonceValue, csrfValue, pkceValue, @@ -463,6 +570,7 @@ func generateValues( func upstreamStateParam( authorizeRequester fosite.AuthorizeRequester, upstreamName string, + upstreamType string, nonceValue nonce.Nonce, csrfValue csrftoken.CSRFToken, pkceValue pkce.Code, @@ -471,6 +579,7 @@ func upstreamStateParam( stateParamData := oidc.UpstreamStateParamData{ AuthParams: authorizeRequester.GetRequestForm().Encode(), UpstreamName: upstreamName, + UpstreamType: upstreamType, Nonce: nonceValue, CSRFToken: csrfValue, PKCECode: pkceValue, diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 51a810de..34e1f158 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -409,23 +409,20 @@ func TestAuthorizationEndpoint(t *testing.T) { return pathWithQuery("/some/path", modifiedHappyGetRequestQueryMap(queryOverrides)) } - expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamNameOverride string) string { + expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamName, upstreamType string) string { csrf := happyCSRF if csrfValueOverride != "" { csrf = csrfValueOverride } - upstreamName := oidcUpstreamName - if upstreamNameOverride != "" { - upstreamName = upstreamNameOverride - } encoded, err := happyStateEncoder.Encode("s", oidctestutil.ExpectedUpstreamStateParamFormat{ P: encodeQuery(modifiedHappyGetRequestQueryMap(queryOverrides)), U: upstreamName, + T: upstreamType, N: happyNonce, C: csrf, K: happyPKCE, - V: "1", + V: "2", }, ) require.NoError(t, err) @@ -558,7 +555,24 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, + { + name: "LDAP upstream browser flow happy path using GET without a CSRF cookie", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPath, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", ldapUpstreamName, "ldap")}), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -639,7 +653,7 @@ func TestAuthorizationEndpoint(t *testing.T) { csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -659,7 +673,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "", wantBodyString: "", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, }, { @@ -748,7 +762,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), nil), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, }, { @@ -767,7 +781,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}), wantUpstreamStateParamInLocationHeader: true, }, { @@ -802,7 +816,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, // Generated a new CSRF cookie and set it in the response. wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -823,7 +837,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client - }, "", ""), nil), + }, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -889,7 +903,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "scope": "openid offline_access", - }, "", ""), nil), + }, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, @@ -1063,7 +1077,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing upstream username on request for LDAP authentication", + name: "missing upstream username but has password on request for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, @@ -1338,21 +1352,45 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "response type is unsupported when using LDAP upstream", + name: "response type is unsupported when using LDAP cli upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), + wantBodyString: "", + }, + { + name: "response type is unsupported when using LDAP browser upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), - wantStatus: http.StatusFound, + wantStatus: http.StatusSeeOther, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, { - name: "response type is unsupported when using active directory upstream", + name: "response type is unsupported when using active directory cli upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), + wantBodyString: "", + }, + { + name: "response type is unsupported when using active directory browser upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), - wantStatus: http.StatusFound, + wantStatus: http.StatusSeeOther, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", @@ -1436,21 +1474,45 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing response type in request using LDAP upstream", + name: "missing response type in request using LDAP cli upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), + wantBodyString: "", + }, + { + name: "missing response type in request using LDAP browser upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), - wantStatus: http.StatusFound, + wantStatus: http.StatusSeeOther, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, { - name: "missing response type in request using Active Directory upstream", + name: "missing response type in request using Active Directory cli upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), + customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), + wantBodyString: "", + }, + { + name: "missing response type in request using Active Directory browser upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), - wantStatus: http.StatusFound, + wantStatus: http.StatusSeeOther, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", @@ -1720,7 +1782,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( - map[string]string{"prompt": "none login", "scope": "email"}, "", "", + map[string]string{"prompt": "none login", "scope": "email"}, "", oidcUpstreamName, "oidc", ), nil), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, @@ -2543,7 +2605,7 @@ func TestAuthorizationEndpoint(t *testing.T) { "scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation "client_id": "some-other-new-client-id", // updated expectation "state": expectedUpstreamStateParam( - nil, "", "some-other-new-idp-name", + nil, "", "some-other-new-idp-name", "oidc", ), // updated expectation "nonce": happyNonce, "code_challenge": expectedUpstreamCodeChallenge, diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 83e2af60..21380979 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -48,7 +48,7 @@ const ( happyDownstreamCSRF = "test-csrf" happyDownstreamPKCE = "test-pkce" happyDownstreamNonce = "test-nonce" - happyDownstreamStateVersion = "1" + happyDownstreamStateVersion = "2" downstreamIssuer = "https://my-downstream-issuer.com/path" downstreamRedirectURI = "http://127.0.0.1/callback" @@ -1162,6 +1162,7 @@ func happyUpstreamStateParam() *upstreamStateParamBuilder { return &upstreamStateParamBuilder{ U: happyUpstreamIDPName, P: happyDownstreamRequestParams, + T: "oidc", N: happyDownstreamNonce, C: happyDownstreamCSRF, K: happyDownstreamPKCE, diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 708d4855..dda7fa86 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package manager @@ -8,6 +8,8 @@ import ( "strings" "sync" + "go.pinniped.dev/internal/oidc/login" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "go.pinniped.dev/internal/oidc" @@ -134,6 +136,8 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs oauthHelperWithKubeStorage, ) + m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler() + plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer) } } diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 769a1d57..1936c406 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -830,6 +830,7 @@ func NewTestUpstreamOIDCIdentityProviderBuilder() *TestUpstreamOIDCIdentityProvi type ExpectedUpstreamStateParamFormat struct { P string `json:"p"` U string `json:"u"` + T string `json:"t"` N string `json:"n"` C string `json:"c"` K string `json:"k"` diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index b2cfd68d..d0cb858b 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -964,6 +964,122 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo expectedGroups, ) }) + + // Add an OIDC upstream IDP and try using it to authenticate during kubectl commands. + t.Run("with Supervisor LDAP upstream IDP and browser flow", func(t *testing.T) { + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. + page := browsertest.Open(t) + + expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + + setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/ldap-test-sessions.yaml" + + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-skip-browser", + "--oidc-ca-bundle", testCABundlePath, + "--upstream-identity-provider-flow", "browser_authcode", + "--oidc-session-cache", sessionCachePath, + }) + + // Run "kubectl get namespaces" which should trigger a browser login via the plugin. + kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + + // Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an + // in-memory buffer, so we can have the full output available to us at the end. + originalStderrPipe, err := kubectlCmd.StderrPipe() + require.NoError(t, err) + originalStdoutPipe, err := kubectlCmd.StdoutPipe() + require.NoError(t, err) + var stderrPipeBuf, stdoutPipeBuf bytes.Buffer + stderrPipe := io.TeeReader(originalStderrPipe, &stderrPipeBuf) + stdoutPipe := io.TeeReader(originalStdoutPipe, &stdoutPipeBuf) + + t.Logf("starting kubectl subprocess") + require.NoError(t, kubectlCmd.Start()) + t.Cleanup(func() { + // Consume readers so that the tee buffers will contain all the output so far. + _, stdoutReadAllErr := readAllCtx(testCtx, stdoutPipe) + _, stderrReadAllErr := readAllCtx(testCtx, stderrPipe) + + // Note that Wait closes the stdout/stderr pipes, so we don't need to close them ourselves. + waitErr := kubectlCmd.Wait() + t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) + + // Upon failure, print the full output so far of the kubectl command. + var testAlreadyFailedErr error + if t.Failed() { + testAlreadyFailedErr = errors.New("test failed prior to clean up function") + } + cleanupErrs := utilerrors.NewAggregate([]error{waitErr, stdoutReadAllErr, stderrReadAllErr, testAlreadyFailedErr}) + + if cleanupErrs != nil { + t.Logf("kubectl stdout was:\n----start of stdout\n%s\n----end of stdout", stdoutPipeBuf.String()) + t.Logf("kubectl stderr was:\n----start of stderr\n%s\n----end of stderr", stderrPipeBuf.String()) + } + require.NoErrorf(t, cleanupErrs, "kubectl process did not exit cleanly and/or the test failed. "+ + "Note: if kubectl's first call to the Pinniped CLI results in the Pinniped CLI returning an error, "+ + "then kubectl may call the Pinniped CLI again, which may hang because it will wait for the user "+ + "to finish the login. This test will kill the kubectl process after a timeout. In this case, the "+ + " kubectl output printed above will include multiple prompts for the user to enter their authcode.", + ) + }) + + // Start a background goroutine to read stderr from the CLI and parse out the login URL. + loginURLChan := make(chan string, 1) + spawnTestGoroutine(testCtx, t, func() error { + reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderrPipe)) + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + loginURL, err := url.Parse(strings.TrimSpace(scanner.Text())) + if err == nil && loginURL.Scheme == "https" { + loginURLChan <- loginURL.String() // this channel is buffered so this will not block + return nil + } + } + return fmt.Errorf("expected stderr to contain login URL") + }) + + // Start a background goroutine to read stdout from kubectl and return the result as a string. + kubectlOutputChan := make(chan string, 1) + spawnTestGoroutine(testCtx, t, func() error { + output, err := readAllCtx(testCtx, stdoutPipe) + if err != nil { + return err + } + t.Logf("kubectl output:\n%s\n", output) + kubectlOutputChan <- string(output) // this channel is buffered so this will not block + return nil + }) + + // Wait for the CLI to print out the login URL and open the browser to it. + t.Logf("waiting for CLI to output login URL") + var loginURL string + select { + case <-time.After(1 * time.Minute): + require.Fail(t, "timed out waiting for login URL") + case loginURL = <-loginURLChan: + } + t.Logf("navigating to login page: %q", loginURL) + require.NoError(t, page.Navigate(loginURL)) + + // Expect to be redirected to the supervisor's ldap login page. + t.Logf("waiting for redirect to supervisor ldap login page") + regex := regexp.MustCompile(`\A` + downstream.Spec.Issuer + `/login.+`) + browsertest.WaitForURL(t, page, regex) + + // TODO actually log in :P + }) } func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) { From 65eed7e74282b08d257b30697ba0f5e301e24f5f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 26 Apr 2022 15:30:39 -0700 Subject: [PATCH 19/77] Implement login_handler.go to defer to other handlers The other handlers for GET and POST requests are not yet implemented in this commit. The shared handler code in login_handler.go takes care of things checking the method, checking the CSRF cookie, decoding the state param, and adding security headers on behalf of both the GET and POST handlers. Some code has been extracted from callback_handler.go to be shared. --- internal/oidc/callback/callback_handler.go | 57 +-- .../oidc/callback/callback_handler_test.go | 37 +- internal/oidc/login/get_login_handler.go | 17 + internal/oidc/login/login_handler.go | 63 ++- internal/oidc/login/login_handler_test.go | 448 ++++++++++++++++++ internal/oidc/login/post_login_handler.go | 19 + internal/oidc/oidc.go | 68 +++ internal/oidc/provider/manager/manager.go | 7 +- .../testutil/oidctestutil/oidctestutil.go | 39 ++ 9 files changed, 656 insertions(+), 99 deletions(-) create mode 100644 internal/oidc/login/get_login_handler.go create mode 100644 internal/oidc/login/login_handler_test.go create mode 100644 internal/oidc/login/post_login_handler.go diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index fbf13728..bcb8bf1b 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -5,7 +5,6 @@ package callback import ( - "crypto/subtle" "net/http" "net/url" @@ -14,7 +13,6 @@ import ( "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" @@ -102,9 +100,9 @@ func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder) return nil, httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET)", r.Method) } - csrfValue, err := readCSRFCookie(r, cookieDecoder) + _, decodedState, err := oidc.ReadStateParamAndValidateCSRFCookie(r, cookieDecoder, stateDecoder) if err != nil { - plog.InfoErr("error reading CSRF cookie", err) + plog.InfoErr("state or CSRF error", err) return nil, err } @@ -113,23 +111,7 @@ func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder) return nil, httperr.New(http.StatusBadRequest, "code param not found") } - if r.FormValue("state") == "" { - plog.Info("state param not found") - return nil, httperr.New(http.StatusBadRequest, "state param not found") - } - - state, err := readState(r, stateDecoder) - if err != nil { - plog.InfoErr("error reading state", err) - return nil, err - } - - if subtle.ConstantTimeCompare([]byte(state.CSRFToken), []byte(csrfValue)) != 1 { - plog.InfoErr("CSRF value does not match", err) - return nil, httperr.Wrap(http.StatusForbidden, "CSRF value does not match", err) - } - - return state, nil + return decodedState, nil } func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister) provider.UpstreamOIDCIdentityProviderI { @@ -140,36 +122,3 @@ func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCId } return nil } - -func readCSRFCookie(r *http.Request, cookieDecoder oidc.Decoder) (csrftoken.CSRFToken, error) { - receivedCSRFCookie, err := r.Cookie(oidc.CSRFCookieName) - if err != nil { - // Error means that the cookie was not found - return "", httperr.Wrap(http.StatusForbidden, "CSRF cookie is missing", err) - } - - var csrfFromCookie csrftoken.CSRFToken - err = cookieDecoder.Decode(oidc.CSRFCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie) - if err != nil { - return "", httperr.Wrap(http.StatusForbidden, "error reading CSRF cookie", err) - } - - return csrfFromCookie, nil -} - -func readState(r *http.Request, stateDecoder oidc.Decoder) (*oidc.UpstreamStateParamData, error) { - var state oidc.UpstreamStateParamData - if err := stateDecoder.Decode( - oidc.UpstreamStateParamEncodingName, - r.FormValue("state"), - &state, - ); err != nil { - return nil, httperr.New(http.StatusBadRequest, "error reading state") - } - - if state.FormatVersion != oidc.UpstreamStateParamFormatVersion { - return nil, httperr.New(http.StatusUnprocessableEntity, "state format version is invalid") - } - - return &state, nil -} diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 21380979..6fc47773 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -1156,10 +1156,8 @@ func (r *requestPath) String() string { return path + params.Encode() } -type upstreamStateParamBuilder oidctestutil.ExpectedUpstreamStateParamFormat - -func happyUpstreamStateParam() *upstreamStateParamBuilder { - return &upstreamStateParamBuilder{ +func happyUpstreamStateParam() *oidctestutil.UpstreamStateParamBuilder { + return &oidctestutil.UpstreamStateParamBuilder{ U: happyUpstreamIDPName, P: happyDownstreamRequestParams, T: "oidc", @@ -1170,37 +1168,6 @@ func happyUpstreamStateParam() *upstreamStateParamBuilder { } } -func (b upstreamStateParamBuilder) Build(t *testing.T, stateEncoder *securecookie.SecureCookie) string { - state, err := stateEncoder.Encode("s", b) - require.NoError(t, err) - return state -} - -func (b *upstreamStateParamBuilder) WithAuthorizeRequestParams(params string) *upstreamStateParamBuilder { - b.P = params - return b -} - -func (b *upstreamStateParamBuilder) WithNonce(nonce string) *upstreamStateParamBuilder { - b.N = nonce - return b -} - -func (b *upstreamStateParamBuilder) WithCSRF(csrf string) *upstreamStateParamBuilder { - b.C = csrf - return b -} - -func (b *upstreamStateParamBuilder) WithPKCVE(pkce string) *upstreamStateParamBuilder { - b.K = pkce - return b -} - -func (b *upstreamStateParamBuilder) WithStateVersion(version string) *upstreamStateParamBuilder { - b.V = version - return b -} - func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(happyUpstreamIDPName). diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go new file mode 100644 index 00000000..e1d6ffb6 --- /dev/null +++ b/internal/oidc/login/get_login_handler.go @@ -0,0 +1,17 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + + "go.pinniped.dev/internal/oidc" +) + +func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { + // TODO + return nil + } +} diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go index 10727b3c..a8e65e0e 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/oidc/login/login_handler.go @@ -5,19 +5,64 @@ package login import ( "net/http" + + idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" + "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/httputil/securityheader" + "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/plog" ) -// NewHandler returns an http.Handler that serves the login endpoint for IDPs that -// don't have their own Web UI. -func NewHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) - return +// HandlerFunc is a function that can handle either a GET or POST request for the login endpoint. +type HandlerFunc func( + w http.ResponseWriter, + r *http.Request, + encodedState string, + decodedState *oidc.UpstreamStateParamData, +) error + +// NewHandler returns a http.Handler that serves the login endpoint for IDPs that don't have their own web UI for login. +// +// This handler takes care of the shared concerns between the GET and POST methods of the login endpoint: +// checking the method, checking the CSRF cookie, decoding the state param, and adding security headers. +// Then it defers the rest of the handling to the passed in handler functions for GET and POST requests. +// Note that CSRF protection isn't needed on GET requests, but it doesn't hurt. Putting it here +// keeps the implementations and tests of HandlerFunc simpler since they won't need to deal with any decoders. +// Users should always initially get redirected to this page from the authorization endpoint, and never need +// to navigate directly to this page in their browser without going through the authorization endpoint first. +// Once their browser has landed on this page, it should be okay for the user to refresh the browser. +func NewHandler( + stateDecoder oidc.Decoder, + cookieDecoder oidc.Decoder, + getHandler HandlerFunc, // use NewGetHandler() for production + postHandler HandlerFunc, // use NewPostHandler() for production +) http.Handler { + loginHandler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + var handler HandlerFunc + switch r.Method { + case http.MethodGet: + handler = getHandler + case http.MethodPost: + handler = postHandler + default: + return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method) } - _, err := w.Write([]byte("

hello world

")) + + encodedState, decodedState, err := oidc.ReadStateParamAndValidateCSRFCookie(r, cookieDecoder, stateDecoder) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + plog.InfoErr("state or CSRF error", err) + return err } + + switch decodedState.UpstreamType { + case string(idpdiscoveryv1alpha1.IDPTypeLDAP), string(idpdiscoveryv1alpha1.IDPTypeActiveDirectory): + // these are the types supported by this endpoint, so no error here + default: + return httperr.Newf(http.StatusBadRequest, "not a supported upstream IDP type for this endpoint: %q", decodedState.UpstreamType) + } + + return handler(w, r, encodedState, decodedState) }) + + return securityheader.Wrap(loginHandler) } diff --git a/internal/oidc/login/login_handler_test.go b/internal/oidc/login/login_handler_test.go new file mode 100644 index 00000000..c77758da --- /dev/null +++ b/internal/oidc/login/login_handler_test.go @@ -0,0 +1,448 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/gorilla/securecookie" + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" +) + +func TestLoginEndpoint(t *testing.T) { + const ( + htmlContentType = "text/html; charset=utf-8" + happyGetResult = "

get handler result

" + happyPostResult = "

post handler result

" + + happyUpstreamIDPName = "upstream-idp-name" + happyUpstreamIDPType = "ldap" + happyDownstreamCSRF = "test-csrf" + happyDownstreamPKCE = "test-pkce" + happyDownstreamNonce = "test-nonce" + happyDownstreamStateVersion = "2" + + downstreamClientID = "pinniped-cli" + happyDownstreamState = "8b-state" + downstreamNonce = "some-nonce-value" + downstreamPKCEChallenge = "some-challenge" + downstreamPKCEChallengeMethod = "S256" + downstreamRedirectURI = "http://127.0.0.1/callback" + ) + + happyDownstreamScopesRequested := []string{"openid"} + happyDownstreamRequestParamsQuery := url.Values{ + "response_type": []string{"code"}, + "scope": []string{strings.Join(happyDownstreamScopesRequested, " ")}, + "client_id": []string{downstreamClientID}, + "state": []string{happyDownstreamState}, + "nonce": []string{downstreamNonce}, + "code_challenge": []string{downstreamPKCEChallenge}, + "code_challenge_method": []string{downstreamPKCEChallengeMethod}, + "redirect_uri": []string{downstreamRedirectURI}, + } + happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode() + + expectedHappyDecodedUpstreamStateParam := func() *oidc.UpstreamStateParamData { + return &oidc.UpstreamStateParamData{ + UpstreamName: happyUpstreamIDPName, + UpstreamType: happyUpstreamIDPType, + AuthParams: happyDownstreamRequestParams, + Nonce: happyDownstreamNonce, + CSRFToken: happyDownstreamCSRF, + PKCECode: happyDownstreamPKCE, + FormatVersion: happyDownstreamStateVersion, + } + } + + expectedHappyDecodedUpstreamStateParamForActiveDirectory := func() *oidc.UpstreamStateParamData { + s := expectedHappyDecodedUpstreamStateParam() + s.UpstreamType = "activedirectory" + return s + } + + happyUpstreamStateParam := func() *oidctestutil.UpstreamStateParamBuilder { + return &oidctestutil.UpstreamStateParamBuilder{ + U: happyUpstreamIDPName, + T: happyUpstreamIDPType, + P: happyDownstreamRequestParams, + N: happyDownstreamNonce, + C: happyDownstreamCSRF, + K: happyDownstreamPKCE, + V: happyDownstreamStateVersion, + } + } + + stateEncoderHashKey := []byte("fake-hash-secret") + stateEncoderBlockKey := []byte("0123456789ABCDEF") // block encryption requires 16/24/32 bytes for AES + cookieEncoderHashKey := []byte("fake-hash-secret2") + cookieEncoderBlockKey := []byte("0123456789ABCDE2") // block encryption requires 16/24/32 bytes for AES + require.NotEqual(t, stateEncoderHashKey, cookieEncoderHashKey) + require.NotEqual(t, stateEncoderBlockKey, cookieEncoderBlockKey) + + happyStateCodec := securecookie.New(stateEncoderHashKey, stateEncoderBlockKey) + happyStateCodec.SetSerializer(securecookie.JSONEncoder{}) + happyCookieCodec := securecookie.New(cookieEncoderHashKey, cookieEncoderBlockKey) + happyCookieCodec.SetSerializer(securecookie.JSONEncoder{}) + + happyState := happyUpstreamStateParam().Build(t, happyStateCodec) + happyPathWithState := newRequestPath().WithState(happyState).String() + + happyActiveDirectoryState := happyUpstreamStateParam().WithUpstreamIDPType("activedirectory").Build(t, happyStateCodec) + + encodedIncomingCookieCSRFValue, err := happyCookieCodec.Encode("csrf", happyDownstreamCSRF) + require.NoError(t, err) + happyCSRFCookie := "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + + tests := []struct { + name string + method string + path string + csrfCookie string + getHandlerErr error + postHandlerErr error + + wantStatus int + wantContentType string + wantBody string + wantEncodedState string + wantDecodedState *oidc.UpstreamStateParamData + }{ + { + name: "PUT method is invalid", + method: http.MethodPut, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: PUT (try GET or POST)\n", + }, + { + name: "PATCH method is invalid", + method: http.MethodPatch, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: PATCH (try GET or POST)\n", + }, + { + name: "DELETE method is invalid", + method: http.MethodDelete, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: DELETE (try GET or POST)\n", + }, + { + name: "HEAD method is invalid", + method: http.MethodHead, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: HEAD (try GET or POST)\n", + }, + { + name: "CONNECT method is invalid", + method: http.MethodConnect, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: CONNECT (try GET or POST)\n", + }, + { + name: "OPTIONS method is invalid", + method: http.MethodOptions, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: OPTIONS (try GET or POST)\n", + }, + { + name: "TRACE method is invalid", + method: http.MethodTrace, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: TRACE (try GET or POST)\n", + }, + { + name: "state param was not included on GET request", + method: http.MethodGet, + path: newRequestPath().WithoutState().String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: state param not found\n", + }, + { + name: "state param was not included on POST request", + method: http.MethodPost, + path: newRequestPath().WithoutState().String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: state param not found\n", + }, + { + name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason on GET request", + method: http.MethodGet, + path: newRequestPath().WithState("this-will-not-decode").String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: error reading state\n", + }, + { + name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason on POST request", + method: http.MethodPost, + path: newRequestPath().WithState("this-will-not-decode").String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: error reading state\n", + }, + { + name: "the CSRF cookie does not exist on GET request", + method: http.MethodGet, + path: happyPathWithState, + csrfCookie: "", + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: CSRF cookie is missing\n", + }, + { + name: "the CSRF cookie does not exist on POST request", + method: http.MethodPost, + path: happyPathWithState, + csrfCookie: "", + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: CSRF cookie is missing\n", + }, + { + name: "the CSRF cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason on GET request", + method: http.MethodGet, + path: happyPathWithState, + csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: error reading CSRF cookie\n", + }, + { + name: "the CSRF cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason on POST request", + method: http.MethodPost, + path: happyPathWithState, + csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: error reading CSRF cookie\n", + }, + { + name: "cookie csrf value does not match state csrf value on GET request", + method: http.MethodGet, + path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: CSRF value does not match\n", + }, + { + name: "cookie csrf value does not match state csrf value on POST request", + method: http.MethodPost, + path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: CSRF value does not match\n", + }, + { + name: "GET request when upstream IDP type in state param is not supported by this endpoint", + method: http.MethodGet, + path: newRequestPath().WithState( + happyUpstreamStateParam().WithUpstreamIDPType("oidc").Build(t, happyStateCodec), + ).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: not a supported upstream IDP type for this endpoint: \"oidc\"\n", + }, + { + name: "POST request when upstream IDP type in state param is not supported by this endpoint", + method: http.MethodPost, + path: newRequestPath().WithState( + happyUpstreamStateParam().WithUpstreamIDPType("oidc").Build(t, happyStateCodec), + ).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: not a supported upstream IDP type for this endpoint: \"oidc\"\n", + }, + { + name: "valid GET request when GET endpoint handler returns an error", + method: http.MethodGet, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + getHandlerErr: httperr.Newf(http.StatusInternalServerError, "some get error"), + wantStatus: http.StatusInternalServerError, + wantContentType: htmlContentType, + wantBody: "Internal Server Error: some get error\n", + wantEncodedState: happyState, + wantDecodedState: expectedHappyDecodedUpstreamStateParam(), + }, + { + name: "valid POST request when POST endpoint handler returns an error", + method: http.MethodPost, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + postHandlerErr: httperr.Newf(http.StatusInternalServerError, "some post error"), + wantStatus: http.StatusInternalServerError, + wantContentType: htmlContentType, + wantBody: "Internal Server Error: some post error\n", + wantEncodedState: happyState, + wantDecodedState: expectedHappyDecodedUpstreamStateParam(), + }, + { + name: "happy GET request for LDAP upstream", + method: http.MethodGet, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: happyGetResult, + wantEncodedState: happyState, + wantDecodedState: expectedHappyDecodedUpstreamStateParam(), + }, + { + name: "happy POST request for LDAP upstream", + method: http.MethodPost, + path: happyPathWithState, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: happyPostResult, + wantEncodedState: happyState, + wantDecodedState: expectedHappyDecodedUpstreamStateParam(), + }, + { + name: "happy GET request for ActiveDirectory upstream", + method: http.MethodGet, + path: newRequestPath().WithState(happyActiveDirectoryState).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: happyGetResult, + wantEncodedState: happyActiveDirectoryState, + wantDecodedState: expectedHappyDecodedUpstreamStateParamForActiveDirectory(), + }, + { + name: "happy POST request for ActiveDirectory upstream", + method: http.MethodPost, + path: newRequestPath().WithState(happyActiveDirectoryState).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: happyPostResult, + wantEncodedState: happyActiveDirectoryState, + wantDecodedState: expectedHappyDecodedUpstreamStateParamForActiveDirectory(), + }, + } + + for _, test := range tests { + tt := test + + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(tt.method, tt.path, nil) + if test.csrfCookie != "" { + req.Header.Set("Cookie", test.csrfCookie) + } + rsp := httptest.NewRecorder() + + testGetHandler := func( + w http.ResponseWriter, + r *http.Request, + encodedState string, + decodedState *oidc.UpstreamStateParamData, + ) error { + require.Equal(t, req, r) + require.Equal(t, rsp, w) + require.Equal(t, tt.wantEncodedState, encodedState) + require.Equal(t, tt.wantDecodedState, decodedState) + if tt.getHandlerErr == nil { + _, err := w.Write([]byte(happyGetResult)) + require.NoError(t, err) + } + return tt.getHandlerErr + } + + testPostHandler := func( + w http.ResponseWriter, + r *http.Request, + encodedState string, + decodedState *oidc.UpstreamStateParamData, + ) error { + require.Equal(t, req, r) + require.Equal(t, rsp, w) + require.Equal(t, tt.wantEncodedState, encodedState) + require.Equal(t, tt.wantDecodedState, decodedState) + if tt.postHandlerErr == nil { + _, err := w.Write([]byte(happyPostResult)) + require.NoError(t, err) + } + return tt.postHandlerErr + } + + subject := NewHandler(happyStateCodec, happyCookieCodec, testGetHandler, testPostHandler) + + subject.ServeHTTP(rsp, req) + + testutil.RequireSecurityHeaders(t, rsp) + + require.Equal(t, tt.wantStatus, rsp.Code) + testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) + require.Equal(t, tt.wantBody, rsp.Body.String()) + }) + } +} + +type requestPath struct { + state *string +} + +func newRequestPath() *requestPath { + return &requestPath{} +} + +func (r *requestPath) WithState(state string) *requestPath { + r.state = &state + return r +} + +func (r *requestPath) WithoutState() *requestPath { + r.state = nil + return r +} + +func (r *requestPath) String() string { + path := "/login?" + params := url.Values{} + if r.state != nil { + params.Add("state", *r.state) + } + return path + params.Encode() +} diff --git a/internal/oidc/login/post_login_handler.go b/internal/oidc/login/post_login_handler.go new file mode 100644 index 00000000..33819c69 --- /dev/null +++ b/internal/oidc/login/post_login_handler.go @@ -0,0 +1,19 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "net/http" + + "github.com/ory/fosite" + + "go.pinniped.dev/internal/oidc" +) + +func NewPostHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { + // TODO + return nil + } +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 9467eb22..90c47655 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -5,12 +5,15 @@ package oidc import ( + "crypto/subtle" + "net/http" "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/ory/fosite" "github.com/ory/fosite/compose" + "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/provider" @@ -297,3 +300,68 @@ func ScopeWasRequested(authorizeRequester fosite.AuthorizeRequester, scopeName s } return false } + +func ReadStateParamAndValidateCSRFCookie(r *http.Request, cookieDecoder Decoder, stateDecoder Decoder) (string, *UpstreamStateParamData, error) { + csrfValue, err := readCSRFCookie(r, cookieDecoder) + if err != nil { + return "", nil, err + } + + encodedState, decodedState, err := readStateParam(r, stateDecoder) + if err != nil { + return "", nil, err + } + + err = validateCSRFValue(decodedState, csrfValue) + if err != nil { + return "", nil, err + } + + return encodedState, decodedState, nil +} + +func readCSRFCookie(r *http.Request, cookieDecoder Decoder) (csrftoken.CSRFToken, error) { + receivedCSRFCookie, err := r.Cookie(CSRFCookieName) + if err != nil { + // Error means that the cookie was not found + return "", httperr.Wrap(http.StatusForbidden, "CSRF cookie is missing", err) + } + + var csrfFromCookie csrftoken.CSRFToken + err = cookieDecoder.Decode(CSRFCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie) + if err != nil { + return "", httperr.Wrap(http.StatusForbidden, "error reading CSRF cookie", err) + } + + return csrfFromCookie, nil +} + +func readStateParam(r *http.Request, stateDecoder Decoder) (string, *UpstreamStateParamData, error) { + encodedState := r.FormValue("state") + + if encodedState == "" { + return "", nil, httperr.New(http.StatusBadRequest, "state param not found") + } + + var state UpstreamStateParamData + if err := stateDecoder.Decode( + UpstreamStateParamEncodingName, + r.FormValue("state"), + &state, + ); err != nil { + return "", nil, httperr.New(http.StatusBadRequest, "error reading state") + } + + if state.FormatVersion != UpstreamStateParamFormatVersion { + return "", nil, httperr.New(http.StatusUnprocessableEntity, "state format version is invalid") + } + + return encodedState, &state, nil +} + +func validateCSRFValue(state *UpstreamStateParamData, csrfCookieValue csrftoken.CSRFToken) error { + if subtle.ConstantTimeCompare([]byte(state.CSRFToken), []byte(csrfCookieValue)) != 1 { + return httperr.New(http.StatusForbidden, "CSRF value does not match") + } + return nil +} diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index dda7fa86..283b1808 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -136,7 +136,12 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs oauthHelperWithKubeStorage, ) - m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler() + m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler( + upstreamStateEncoder, + csrfCookieEncoder, + login.NewGetHandler(m.upstreamIDPs), + login.NewPostHandler(m.upstreamIDPs, oauthHelperWithKubeStorage), + ) plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer) } diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index 1936c406..c408ada9 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -15,6 +15,7 @@ import ( "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/securecookie" "github.com/ory/fosite" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -837,6 +838,44 @@ type ExpectedUpstreamStateParamFormat struct { V string `json:"v"` } +type UpstreamStateParamBuilder ExpectedUpstreamStateParamFormat + +func (b UpstreamStateParamBuilder) Build(t *testing.T, stateEncoder *securecookie.SecureCookie) string { + state, err := stateEncoder.Encode("s", b) + require.NoError(t, err) + return state +} + +func (b *UpstreamStateParamBuilder) WithAuthorizeRequestParams(params string) *UpstreamStateParamBuilder { + b.P = params + return b +} + +func (b *UpstreamStateParamBuilder) WithNonce(nonce string) *UpstreamStateParamBuilder { + b.N = nonce + return b +} + +func (b *UpstreamStateParamBuilder) WithCSRF(csrf string) *UpstreamStateParamBuilder { + b.C = csrf + return b +} + +func (b *UpstreamStateParamBuilder) WithPKCE(pkce string) *UpstreamStateParamBuilder { + b.K = pkce + return b +} + +func (b *UpstreamStateParamBuilder) WithUpstreamIDPType(upstreamIDPType string) *UpstreamStateParamBuilder { + b.T = upstreamIDPType + return b +} + +func (b *UpstreamStateParamBuilder) WithStateVersion(version string) *UpstreamStateParamBuilder { + b.V = version + return b +} + type staticKeySet struct { publicKey crypto.PublicKey } From 379a80350976c31763b786aeb4cbf914d998197f Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 26 Apr 2022 16:46:58 -0700 Subject: [PATCH 20/77] when password header but not username is sent to password grant, error also add more unit tests Signed-off-by: Margo Crawford --- internal/oidc/auth/auth_handler.go | 6 +- internal/oidc/auth/auth_handler_test.go | 109 +++++++++++++++++++++++- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 5b4d0bcb..d658100c 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -63,7 +63,8 @@ func NewHandler( } if idpType == psession.ProviderTypeOIDC { - if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 { + if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 || + len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 { // The client set a username header, so they are trying to log in with a username/password. return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) } @@ -78,7 +79,8 @@ func NewHandler( } // we know it's an AD/LDAP upstream. - if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 || len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 { + if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 || + len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 { // The client set a username header, so they are trying to log in with a username/password. return handleAuthRequestForLDAPUpstreamCLIFlow(r, w, oauthHelperWithStorage, diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 34e1f158..128d5d4f 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -576,6 +576,23 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPath, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", activeDirectoryUpstreamName, "activedirectory")}), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, { name: "OIDC upstream password grant happy path using GET", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), @@ -599,7 +616,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { - name: "LDAP upstream happy path using GET", + name: "LDAP cli upstream happy path using GET", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, @@ -620,7 +637,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, { - name: "ActiveDirectory upstream happy path using GET", + name: "ActiveDirectory cli upstream happy path using GET", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodGet, path: happyGetRequestPath, @@ -657,6 +674,40 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "LDAP upstream browser flow happy path using GET with a CSRF cookie", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPath, + csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ldapUpstreamName, "ldap")}), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, + { + name: "Active Directory upstream browser flow happy path using GET with a CSRF cookie", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPath, + csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, incomingCookieCSRFValue, activeDirectoryUpstreamName, "activedirectory")}), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, { name: "OIDC upstream browser flow happy path using POST", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), @@ -676,6 +727,44 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, }, + { + name: "LDAP upstream browser flow happy path using POST", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodPost, + path: "/some/path", + contentType: "application/x-www-form-urlencoded", + body: encodeQuery(happyGetRequestQueryMap), + wantStatus: http.StatusSeeOther, + wantContentType: "", + wantBodyString: "", + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", ldapUpstreamName, "ldap")}), + wantUpstreamStateParamInLocationHeader: true, + }, + { + name: "Active Directory upstream browser flow happy path using POST", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodPost, + path: "/some/path", + contentType: "application/x-www-form-urlencoded", + body: encodeQuery(happyGetRequestQueryMap), + wantStatus: http.StatusSeeOther, + wantContentType: "", + wantBodyString: "", + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", activeDirectoryUpstreamName, "activedirectory")}), + wantUpstreamStateParamInLocationHeader: true, + }, { name: "OIDC upstream password grant happy path using POST", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), @@ -701,7 +790,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, }, { - name: "LDAP upstream happy path using POST", + name: "LDAP cli upstream happy path using POST", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodPost, path: "/some/path", @@ -724,7 +813,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, }, { - name: "Active Directory upstream happy path using POST", + name: "Active Directory cli upstream happy path using POST", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodPost, path: "/some/path", @@ -1076,6 +1165,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "missing upstream username but has password on request for OIDC password grant", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: nil, // do not send header + customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, { name: "missing upstream username but has password on request for LDAP authentication", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), From ae60d4356b28e78a7d275776321405342f9f9b83 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 27 Apr 2022 08:51:37 -0700 Subject: [PATCH 21/77] Some refactoring of shared code between OIDC and LDAP browser flows Signed-off-by: Margo Crawford --- internal/oidc/auth/auth_handler.go | 203 ++++++++++++++--------------- 1 file changed, 100 insertions(+), 103 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index d658100c..5f74b8bd 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -172,62 +172,23 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, ) error { - authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) - if !created { - return nil - } - - now := time.Now() - _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ - Fosite: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. - Subject: "none", - AuthTime: now, - RequestedAt: now, - }, - }, - }) - if err != nil { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false) - } - - csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) - if err != nil { - plog.Error("authorize generate error", err) - return err - } - csrfFromCookie := readCSRFCookie(r, cookieCodec) - if csrfFromCookie != "" { - csrfValue = csrfFromCookie - } - - encodedStateParamValue, err := upstreamStateParam( - authorizeRequester, + encodedStateParamValue, _, _, err := handleBrowserAuthRequest( + r, + w, + oauthHelper, + generateCSRF, + generateNonce, + generatePKCE, ldapUpstream.GetName(), - string(idpType), - nonceValue, - csrfValue, - pkceValue, + idpType, + cookieCodec, upstreamStateEncoder, ) if err != nil { - plog.Error("authorize upstream state param error", err) return err } - - promptParam := r.Form.Get(promptParamName) - if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) - } - - if csrfFromCookie == "" { - // We did not receive an incoming CSRF cookie, so write a new one. - err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) - if err != nil { - plog.Error("error setting CSRF cookie", err) - return err - } + if encodedStateParamValue == "" { + return nil } loginURL, err := url.Parse(downstreamIssuer + "/login") @@ -312,34 +273,23 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, ) error { - authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) - if !created { - return nil - } - - now := time.Now() - _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ - Fosite: &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. - Subject: "none", - AuthTime: now, - RequestedAt: now, - }, - }, - }) + encodedStateParamValue, pkceValue, nonceValue, err := handleBrowserAuthRequest( + r, + w, + oauthHelper, + generateCSRF, + generateNonce, + generatePKCE, + oidcUpstream.GetName(), + psession.ProviderTypeOIDC, + cookieCodec, + upstreamStateEncoder, + ) if err != nil { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false) - } - - csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) - if err != nil { - plog.Error("authorize generate error", err) return err } - csrfFromCookie := readCSRFCookie(r, cookieCodec) - if csrfFromCookie != "" { - csrfValue = csrfFromCookie + if encodedStateParamValue == "" { + return nil } upstreamOAuthConfig := oauth2.Config{ @@ -351,44 +301,16 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( Scopes: oidcUpstream.GetScopes(), } - encodedStateParamValue, err := upstreamStateParam( - authorizeRequester, - oidcUpstream.GetName(), - string(psession.ProviderTypeOIDC), - nonceValue, - csrfValue, - pkceValue, - upstreamStateEncoder, - ) - if err != nil { - plog.Error("authorize upstream state param error", err) - return err - } - authCodeOptions := []oauth2.AuthCodeOption{ nonceValue.Param(), pkceValue.Challenge(), pkceValue.Method(), } - promptParam := r.Form.Get(promptParamName) - if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) - } - for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val)) } - if csrfFromCookie == "" { - // We did not receive an incoming CSRF cookie, so write a new one. - err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) - if err != nil { - plog.Error("error setting CSRF cookie", err) - return err - } - } - http.Redirect(w, r, upstreamOAuthConfig.AuthCodeURL( encodedStateParamValue, @@ -549,6 +471,81 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider } } +// handleBrowserAuthRequest performs the shared validations and setup between browser based auth requests +// regardless of IDP type-- LDAP, Active Directory and OIDC. +// It generates the state param, sets the CSRF cookie, and validates the prompt param. +func handleBrowserAuthRequest( + r *http.Request, + w http.ResponseWriter, + oauthHelper fosite.OAuth2Provider, + generateCSRF func() (csrftoken.CSRFToken, error), + generateNonce func() (nonce.Nonce, error), + generatePKCE func() (pkce.Code, error), + upstreamName string, + idpType psession.ProviderType, + cookieCodec oidc.Codec, + upstreamStateEncoder oidc.Encoder, +) (string, pkce.Code, nonce.Nonce, error) { + authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) + if !created { + return "", "", "", nil + } + + now := time.Now() + _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ + Fosite: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. + Subject: "none", + AuthTime: now, + RequestedAt: now, + }, + }, + }) + if err != nil { + return "", "", "", writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false) + } + + csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) + if err != nil { + plog.Error("authorize generate error", err) + return "", "", "", err + } + csrfFromCookie := readCSRFCookie(r, cookieCodec) + if csrfFromCookie != "" { + csrfValue = csrfFromCookie + } + + encodedStateParamValue, err := upstreamStateParam( + authorizeRequester, + upstreamName, + string(idpType), + nonceValue, + csrfValue, + pkceValue, + upstreamStateEncoder, + ) + if err != nil { + plog.Error("authorize upstream state param error", err) + return "", "", "", err + } + + promptParam := r.Form.Get(promptParamName) + if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { + return "", "", "", writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) + } + + if csrfFromCookie == "" { + // We did not receive an incoming CSRF cookie, so write a new one. + err = addCSRFSetCookieHeader(w, csrfValue, cookieCodec) + if err != nil { + plog.Error("error setting CSRF cookie", err) + return "", "", "", err + } + } + return encodedStateParamValue, pkceValue, nonceValue, nil +} + func generateValues( generateCSRF func() (csrftoken.CSRFToken, error), generateNonce func() (nonce.Nonce, error), From 77f016fb64d91446aafcae57d9e6bd3db3d40ff5 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 27 Apr 2022 08:53:53 -0700 Subject: [PATCH 22/77] Allow browser_authcode flow for pinniped login command Signed-off-by: Margo Crawford --- cmd/pinniped/cmd/login_oidc.go | 6 +++--- cmd/pinniped/cmd/login_oidc_test.go | 32 +++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index c8b2b0cc..bf35a6ba 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd @@ -271,11 +271,11 @@ func flowOptions(requestedIDPType idpdiscoveryv1alpha1.IDPType, requestedFlow id case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "": return useCLIFlow, nil case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode: - fallthrough // not supported for LDAP providers, so fallthrough to error case + return nil, nil default: return nil, fmt.Errorf( "--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)", - requestedIDPType, requestedFlow, []string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}) + requestedIDPType, requestedFlow, strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", ")) } default: // Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236 diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 4a384c76..da0cfcb7 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -235,18 +235,30 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 5, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, + { + name: "ldap upstream type with browser_authcode flow is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "ldap", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + wantOptionsCount: 4, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, { name: "ldap upstream type with unsupported flow is an error", args: []string{ "--issuer", "test-issuer", "--client-id", "test-client-id", "--upstream-identity-provider-type", "ldap", - "--upstream-identity-provider-flow", "browser_authcode", // "browser_authcode" is only supported for OIDC upstreams + "--upstream-identity-provider-flow", "foo", "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution }, wantError: true, wantStderr: here.Doc(` - Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": browser_authcode (supported values: [cli_password]) + Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode) `), }, { @@ -261,18 +273,30 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 5, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, + { + name: "active directory upstream type with browser_authcode is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "activedirectory", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + wantOptionsCount: 4, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, { name: "active directory upstream type with unsupported flow is an error", args: []string{ "--issuer", "test-issuer", "--client-id", "test-client-id", "--upstream-identity-provider-type", "activedirectory", - "--upstream-identity-provider-flow", "browser_authcode", // "browser_authcode" is only supported for OIDC upstreams + "--upstream-identity-provider-flow", "foo", "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution }, wantError: true, wantStderr: here.Doc(` - Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": browser_authcode (supported values: [cli_password]) + Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode) `), }, { From 07b2306254fef878f0cb566a340a134f674fb397 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 28 Apr 2022 09:11:51 -0700 Subject: [PATCH 23/77] Add basic outline of login get handler Signed-off-by: Margo Crawford --- internal/oidc/login/get_login_handler.go | 21 ++++- internal/oidc/login/get_login_handler_test.go | 81 +++++++++++++++++++ internal/oidc/login/login_form.gohtml | 32 ++++++++ internal/oidc/login/login_handler_test.go | 5 +- test/integration/e2e_test.go | 1 + 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 internal/oidc/login/get_login_handler_test.go create mode 100644 internal/oidc/login/login_form.gohtml diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go index e1d6ffb6..c7bc335e 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/oidc/login/get_login_handler.go @@ -4,14 +4,33 @@ package login import ( + _ "embed" + "html/template" "net/http" "go.pinniped.dev/internal/oidc" ) +var ( + //go:embed login_form.gohtml + rawHTMLTemplate string +) + +var parsedHTMLTemplate = template.Must(template.New("login_post.gohtml").Parse(rawHTMLTemplate)) + +type PageData struct { + State string + IDPName string +} + func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { - // TODO + + err := parsedHTMLTemplate.Execute(w, &PageData{State: encodedState, IDPName: decodedState.UpstreamName}) + if err != nil { + return err + } + return nil } } diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go new file mode 100644 index 00000000..cbb48103 --- /dev/null +++ b/internal/oidc/login/get_login_handler_test.go @@ -0,0 +1,81 @@ +package login + +import ( + "net/http" + "net/http/httptest" + "testing" + + "go.pinniped.dev/internal/testutil" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/oidc" +) + +func TestGetLogin(t *testing.T) { + const ( + happyLdapIDPName = "some-ldap-idp" + happyGetResult = ` + + + +

Pinniped

+

some-ldap-idp

+ +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +` + ) + tests := []struct { + name string + decodedState *oidc.UpstreamStateParamData + encodedState string + idps oidc.UpstreamIdentityProvidersLister + wantStatus int + wantContentType string + wantBody string + }{ + { + name: "Happy path ldap", + decodedState: &oidc.UpstreamStateParamData{ + UpstreamName: happyLdapIDPName, + UpstreamType: "ldap", + }, + encodedState: "foo", // the encoded and decoded state don't match, but that verification is handled one level up. + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: happyGetResult, + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + handler := NewGetHandler(tt.idps) + req := httptest.NewRequest(http.MethodGet, "/login", nil) + rsp := httptest.NewRecorder() + err := handler(rsp, req, tt.encodedState, tt.decodedState) + require.NoError(t, err) + + require.Equal(t, test.wantStatus, rsp.Code) + testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) + body := rsp.Body.String() + require.Equal(t, tt.wantBody, body) + }) + } +} diff --git a/internal/oidc/login/login_form.gohtml b/internal/oidc/login/login_form.gohtml new file mode 100644 index 00000000..adb5c2a9 --- /dev/null +++ b/internal/oidc/login/login_form.gohtml @@ -0,0 +1,32 @@ + + + + +

Pinniped

+

{{ .IDPName }}

+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + diff --git a/internal/oidc/login/login_handler_test.go b/internal/oidc/login/login_handler_test.go index c77758da..347f0760 100644 --- a/internal/oidc/login/login_handler_test.go +++ b/internal/oidc/login/login_handler_test.go @@ -19,9 +19,12 @@ import ( "go.pinniped.dev/internal/testutil/oidctestutil" ) +const ( + htmlContentType = "text/html; charset=utf-8" +) + func TestLoginEndpoint(t *testing.T) { const ( - htmlContentType = "text/html; charset=utf-8" happyGetResult = "

get handler result

" happyPostResult = "

post handler result

" diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index d0cb858b..13a70757 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -1078,6 +1078,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo regex := regexp.MustCompile(`\A` + downstream.Spec.Issuer + `/login.+`) browsertest.WaitForURL(t, page, regex) + browsertest.WaitForVisibleElements(t, page, "input#username", "input#password", "button#submit") // TODO actually log in :P }) } From 453c69af7d6b321f72cc739750142eac6fbae06b Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 28 Apr 2022 12:07:04 -0700 Subject: [PATCH 24/77] Fix some errors and pass state as form element Signed-off-by: Margo Crawford --- internal/oidc/login/get_login_handler.go | 4 +--- internal/oidc/login/get_login_handler_test.go | 19 +++++++++++++------ internal/oidc/login/login_form.gohtml | 8 ++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go index c7bc335e..18b3ba8c 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/oidc/login/get_login_handler.go @@ -16,16 +16,14 @@ var ( rawHTMLTemplate string ) -var parsedHTMLTemplate = template.Must(template.New("login_post.gohtml").Parse(rawHTMLTemplate)) - type PageData struct { State string IDPName string } func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFunc { + var parsedHTMLTemplate = template.Must(template.New("login_post.gohtml").Parse(rawHTMLTemplate)) return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { - err := parsedHTMLTemplate.Execute(w, &PageData{State: encodedState, IDPName: decodedState.UpstreamName}) if err != nil { return err diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index cbb48103..97ebf990 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -1,3 +1,6 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package login import ( @@ -22,19 +25,23 @@ func TestGetLogin(t *testing.T) {

Pinniped

some-ldap-idp

-
+
- - + +
- - + +
- +
+ +
+ +
diff --git a/internal/oidc/login/login_form.gohtml b/internal/oidc/login/login_form.gohtml index adb5c2a9..7bc61878 100644 --- a/internal/oidc/login/login_form.gohtml +++ b/internal/oidc/login/login_form.gohtml @@ -8,15 +8,15 @@ SPDX-License-Identifier: Apache-2.0

Pinniped

{{ .IDPName }}

-
+
- +
- +
@@ -24,7 +24,7 @@ SPDX-License-Identifier: Apache-2.0 - +
From 2cdb55e7da90e2783c81be12f28f6464d0d6c5be Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Thu, 28 Apr 2022 15:31:50 -0400 Subject: [PATCH 25/77] Bump deps to latest and go mod compat to 1.17 Signed-off-by: Monis Khan --- go.mod | 74 ++++++++++++------------- go.sum | 148 +++++++++++++++++++++++++++++-------------------- hack/module.sh | 2 +- 3 files changed, 125 insertions(+), 99 deletions(-) diff --git a/go.mod b/go.mod index d56bdbf8..251b43e2 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/go-logr/stdr v1.2.2 github.com/gofrs/flock v0.8.1 github.com/golang/mock v1.6.0 - github.com/google/go-cmp v0.5.7 + github.com/google/go-cmp v0.5.8 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.3.0 github.com/gorilla/securecookie v1.1.1 @@ -62,13 +62,13 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.1 - github.com/tdewolff/minify/v2 v2.11.1 + github.com/tdewolff/minify/v2 v2.11.2 go.uber.org/atomic v1.9.0 - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 - golang.org/x/net v0.0.0-20220225172249-27dd8689420f - golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 + golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 golang.org/x/text v0.3.7 gopkg.in/square/go-jose.v2 v2.6.0 k8s.io/api v0.23.6 @@ -85,17 +85,15 @@ require ( ) require ( - cloud.google.com/go/compute v1.5.0 // indirect + cloud.google.com/go/compute v1.6.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.24 // indirect + github.com/Azure/go-autorest/autorest v0.11.27 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -103,19 +101,19 @@ require ( github.com/coreos/go-oidc v2.2.1+incompatible // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/emicklei/go-restful v2.15.0+incompatible // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.21.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.3.0 // indirect + github.com/golang-jwt/jwt/v4 v4.4.1 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -132,54 +130,54 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/goveralls v0.0.11 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/ory/go-acc v0.2.7 // indirect + github.com/ory/go-acc v0.2.8 // indirect github.com/ory/go-convenience v0.1.0 // indirect github.com/ory/viper v1.7.5 // indirect - github.com/ory/x v0.0.214 // indirect + github.com/ory/x v0.0.380 // indirect github.com/pborman/uuid v1.2.1 // indirect - github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/common v0.34.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/afero v1.8.1 // indirect + github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/tdewolff/parse/v2 v2.5.28 // indirect - go.etcd.io/etcd/api/v3 v3.5.2 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect - go.etcd.io/etcd/client/v3 v3.5.2 // indirect + github.com/tdewolff/parse/v2 v2.5.29 // indirect + go.etcd.io/etcd/api/v3 v3.5.4 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect + go.etcd.io/etcd/client/v3 v3.5.4 // indirect go.opentelemetry.io/contrib v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect - go.opentelemetry.io/otel v1.2.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect + go.opentelemetry.io/otel v1.6.3 // indirect go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect - go.opentelemetry.io/otel/metric v0.20.0 // indirect - go.opentelemetry.io/otel/sdk v1.2.0 // indirect + go.opentelemetry.io/otel/metric v0.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.6.3 // indirect go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect - go.opentelemetry.io/otel/trace v1.2.0 // indirect - go.opentelemetry.io/proto/otlp v0.10.0 // indirect + go.opentelemetry.io/otel/trace v1.6.3 // indirect + go.opentelemetry.io/proto/otlp v0.15.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/mod v0.5.1 // indirect - golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect - golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect - golang.org/x/tools v0.1.9 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect + golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect + golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect + golang.org/x/tools v0.1.10 // indirect + golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 // indirect - google.golang.org/grpc v1.44.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46 // indirect + google.golang.org/grpc v1.46.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 9dda3018..b04534a9 100644 --- a/go.sum +++ b/go.sum @@ -30,7 +30,6 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= 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= @@ -40,8 +39,10 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= 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= @@ -62,15 +63,16 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= +github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= @@ -94,9 +96,7 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -142,7 +142,6 @@ github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4r github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -160,6 +159,7 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= @@ -187,16 +187,15 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd 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 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= @@ -247,6 +246,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= @@ -266,8 +266,9 @@ github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9 github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.3/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -282,11 +283,13 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI= github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -320,8 +323,8 @@ github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3Hfo github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= @@ -626,8 +629,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= -github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/gddo v0.0.0-20180828051604-96d2a289f41e/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= @@ -690,8 +693,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -727,6 +731,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= @@ -972,8 +978,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/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= @@ -1045,8 +1051,8 @@ github.com/ory/fosite v0.42.2 h1:fKfGAgMmmeM1C0DXCyt5TOzQWrKmLOL+PApEC4bIv2o= github.com/ory/fosite v0.42.2/go.mod h1:qggrqm3ZWQF9i2f/d3RLH5mHHPtv44hsiltkVKLsCYo= github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90/go.mod h1:sxnvPCxChFuSmTJGj8FdMupeq1BezCiEpDjTUXQ4hf4= github.com/ory/go-acc v0.2.6/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw= -github.com/ory/go-acc v0.2.7 h1:VjSz+Xj3LoJRBmAXt9cxuL1lanO/Bvf9ajny5NvwbtM= -github.com/ory/go-acc v0.2.7/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw= +github.com/ory/go-acc v0.2.8 h1:rOHHAPQjf0u7eHFGWpiXK+gIu/e0GRSJNr9pDukdNC4= +github.com/ory/go-acc v0.2.8/go.mod h1:iCRZUdGb/7nqvSn8xWZkhfVrtXRZ9Wru2E5rabCjFPI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs= github.com/ory/gojsonreference v0.0.0-20190720135523-6b606c2d8ee8/go.mod h1:wsH1C4nIeeQClDtD5AH7kF1uTS6zWyqfjVDTmB0Em7A= @@ -1064,8 +1070,9 @@ github.com/ory/x v0.0.84/go.mod h1:RXLPBG7B+hAViONVg0sHwK+U/ie1Y/NeXrq1JcARfoE= github.com/ory/x v0.0.93/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g= github.com/ory/x v0.0.110/go.mod h1:DJfkE3GdakhshNhw4zlKoRaL/ozg/lcTahA9OCih2BE= github.com/ory/x v0.0.127/go.mod h1:FwUujfFuCj5d+xgLn4fGMYPnzriR5bdAIulFXMtnK0M= -github.com/ory/x v0.0.214 h1:nz5ijvm5MVhYxWsQSuUrW1hj9F5QLZvPn/nLo5s06T4= github.com/ory/x v0.0.214/go.mod h1:aRl57gzyD4GF0HQCekovXhv0xTZgAgiht3o8eVhsm9Q= +github.com/ory/x v0.0.380 h1:A7QYsVQQQ0CgW9Do0+Z8QkeFNaKgXsfQ/MChQm00s9U= +github.com/ory/x v0.0.380/go.mod h1:JHPSavhYHgzlh9teE1vGY+1tecUo2CzfLqHex42jNSQ= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -1077,8 +1084,8 @@ github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/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= @@ -1104,6 +1111,7 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -1117,8 +1125,9 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE= +github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -1146,7 +1155,6 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -1207,8 +1215,8 @@ github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk= -github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 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= @@ -1257,10 +1265,10 @@ github.com/stretchr/testify v1.7.1/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.11.1 h1:x2IAGnHs3qBjulArA7g4dYGCpcMrM8H2sywfwr436RA= -github.com/tdewolff/minify/v2 v2.11.1/go.mod h1:UkCTT2Sa8N7XNU0Z9Q+De6NvaxPlC7DGfSWDRowwXqY= -github.com/tdewolff/parse/v2 v2.5.28 h1:QziFVLe+bfFIwnCWAJzMrzwltQXPT21Evl9Z4x25D+U= -github.com/tdewolff/parse/v2 v2.5.28/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/minify/v2 v2.11.2 h1:PpaPWhNlMVjkAKaOj0bbPv6KCVnrm8jbVwG7OtSdAqw= +github.com/tdewolff/minify/v2 v2.11.2/go.mod h1:NxozhBtgUVypPLzQdV96wkIu9J9vAiVmBcKhfC2zMfg= +github.com/tdewolff/parse/v2 v2.5.29 h1:Uf0OtZL9YaUXTuHEOitdo9lD90P0XTwCjZi+KbGChuM= +github.com/tdewolff/parse/v2 v2.5.29/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= @@ -1300,7 +1308,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/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= @@ -1311,16 +1318,16 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.2 h1:tXok5yLlKyuQ/SXSjtqHc4uzNaMqZi2XsoSPr/LlJXI= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.2 h1:4hzqQ6hIb3blLyQ8usCU4h3NghkqcsohEQ3o3VetYxE= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/client/v3 v3.5.2 h1:WdnejrUtQC4nCxK0/dLTMqKOB+U5TP/2Ya0BJL+1otA= -go.etcd.io/etcd/client/v3 v3.5.2/go.mod h1:kOOaWFFgHygyT0WlSmL8TJiXmMysO/nNUlEsSsN6W4o= +go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.etcd.io/etcd/pkg/v3 v3.5.0 h1:ntrg6vvKRW26JRmHTE0iNlDgYK6JX3hg/4cD62X0ixk= go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0 h1:kw2TmO3yFTgE+F0mdKkG7xMxkit2duBDa2Hu6D/HMlw= @@ -1427,8 +1434,9 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1469,8 +1477,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1531,17 +1539,18 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v 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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1560,8 +1569,10 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 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= @@ -1662,7 +1673,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1678,20 +1688,24 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/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-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1707,8 +1721,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= -golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= +golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1813,15 +1827,16 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= -golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= @@ -1862,6 +1877,9 @@ google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3h google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1943,8 +1961,15 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 h1:ntPPoHzFW6Xp09ueznmahONZufyoSakK/piXnr2BU3I= -google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46 h1:G1IeWbjrqEq9ChWxEuRPJu6laA67+XgTFHVSAvepr38= +google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1975,8 +2000,10 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1991,8 +2018,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= diff --git a/hack/module.sh b/hack/module.sh index b421c7b2..d4fa9873 100755 --- a/hack/module.sh +++ b/hack/module.sh @@ -9,7 +9,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" function tidy_cmd() { local version="$(cat "${ROOT}/go.mod" | grep '^go ' | cut -f 2 -d ' ')" - echo "go mod tidy -v -go=${version} -compat=1.16" + echo "go mod tidy -v -go=${version} -compat=1.17" } function lint_cmd() { From 646c6ec9ed18e5543f31219b3879779444ca98dc Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 29 Apr 2022 10:36:13 -0700 Subject: [PATCH 26/77] Show error message on login page Also add autocomplete attribute and title element Signed-off-by: Margo Crawford --- internal/oidc/login/get_login_handler.go | 26 +++- internal/oidc/login/get_login_handler_test.go | 127 +++++++++++++----- internal/oidc/login/login_form.gohtml | 15 ++- 3 files changed, 130 insertions(+), 38 deletions(-) diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go index 18b3ba8c..a8f90216 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/oidc/login/get_login_handler.go @@ -11,20 +11,40 @@ import ( "go.pinniped.dev/internal/oidc" ) +const defaultErrorMessage = "An internal error occurred. Please contact your administrator for help." + var ( //go:embed login_form.gohtml rawHTMLTemplate string + + errorMappings = map[string]string{ + "login_error": "Incorrect username or password.", + } ) type PageData struct { - State string - IDPName string + State string + IDPName string + HasAlertError bool + AlertMessage string + Title string } func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFunc { var parsedHTMLTemplate = template.Must(template.New("login_post.gohtml").Parse(rawHTMLTemplate)) return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { - err := parsedHTMLTemplate.Execute(w, &PageData{State: encodedState, IDPName: decodedState.UpstreamName}) + alertError := r.URL.Query().Get("err") + message := errorMappings[alertError] + if message == "" { + message = defaultErrorMessage + } + err := parsedHTMLTemplate.Execute(w, &PageData{ + State: encodedState, + IDPName: decodedState.UpstreamName, + HasAlertError: alertError != "", + AlertMessage: message, + Title: "Pinniped", + }) if err != nil { return err } diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index 97ebf990..7e4d8c56 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -4,6 +4,7 @@ package login import ( + "fmt" "net/http" "net/http/httptest" "testing" @@ -18,40 +19,13 @@ import ( func TestGetLogin(t *testing.T) { const ( happyLdapIDPName = "some-ldap-idp" - happyGetResult = ` - - - -

Pinniped

-

some-ldap-idp

- -
- -
- - -
- -
- - -
- -
- -
- - - -
- - -` ) + tests := []struct { name string decodedState *oidc.UpstreamStateParamData encodedState string + errParam string idps oidc.UpstreamIdentityProvidersLister wantStatus int wantContentType string @@ -66,7 +40,57 @@ func TestGetLogin(t *testing.T) { encodedState: "foo", // the encoded and decoded state don't match, but that verification is handled one level up. wantStatus: http.StatusOK, wantContentType: htmlContentType, - wantBody: happyGetResult, + wantBody: getHTMLResult(""), + }, + { + name: "displays error banner when err=login_error param is sent", + decodedState: &oidc.UpstreamStateParamData{ + UpstreamName: happyLdapIDPName, + UpstreamType: "ldap", + }, + encodedState: "foo", + errParam: "login_error", + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: getHTMLResult(` +
+ Incorrect username or password. +
+`), + }, + { + name: "displays error banner when err=internal_error param is sent", + decodedState: &oidc.UpstreamStateParamData{ + UpstreamName: happyLdapIDPName, + UpstreamType: "ldap", + }, + encodedState: "foo", + errParam: "internal_error", + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: getHTMLResult(` +
+ An internal error occurred. Please contact your administrator for help. +
+`), + }, + // If we get an error that we don't recognize, that's also an error, so we + // should probably just tell you to contact your administrator... + { + name: "displays generic error banner when unrecognized err param is sent", + decodedState: &oidc.UpstreamStateParamData{ + UpstreamName: happyLdapIDPName, + UpstreamType: "ldap", + }, + encodedState: "foo", + errParam: "some_other_error", + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBody: getHTMLResult(` +
+ An internal error occurred. Please contact your administrator for help. +
+`), }, } @@ -74,7 +98,11 @@ func TestGetLogin(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { handler := NewGetHandler(tt.idps) - req := httptest.NewRequest(http.MethodGet, "/login", nil) + target := "/login?state=" + tt.encodedState + if tt.errParam != "" { + target += "&err=" + tt.errParam + } + req := httptest.NewRequest(http.MethodGet, target, nil) rsp := httptest.NewRecorder() err := handler(rsp, req, tt.encodedState, tt.decodedState) require.NoError(t, err) @@ -86,3 +114,40 @@ func TestGetLogin(t *testing.T) { }) } } + +func getHTMLResult(errorBanner string) string { + happyGetResult := ` + + + Pinniped + + + +

Pinniped

+

some-ldap-idp

+%s +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + +` + return fmt.Sprintf(happyGetResult, errorBanner) +} diff --git a/internal/oidc/login/login_form.gohtml b/internal/oidc/login/login_form.gohtml index 7bc61878..5376b5e4 100644 --- a/internal/oidc/login/login_form.gohtml +++ b/internal/oidc/login/login_form.gohtml @@ -3,28 +3,35 @@ Copyright 2022 the Pinniped contributors. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 --> + + {{.Title}} +

Pinniped

{{ .IDPName }}

- +{{if .HasAlertError}} +
+ {{.AlertMessage}} +
+{{end}}
- +
- +
- +
From 69e5169fc52fd3cd394834d3f52371607bfce34d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 29 Apr 2022 16:01:51 -0700 Subject: [PATCH 27/77] Implement post_login_handler.go to accept form post and auth to LDAP/AD Also extract some helpers from auth_handler.go so they can be shared with the new handler. --- internal/oidc/auth/auth_handler.go | 154 +--- internal/oidc/auth/auth_handler_test.go | 176 ++--- .../downstreamsession/downstream_session.go | 34 + internal/oidc/login/login_handler.go | 41 ++ internal/oidc/login/post_login_handler.go | 73 +- .../oidc/login/post_login_handler_test.go | 693 ++++++++++++++++++ internal/oidc/oidc.go | 110 +++ internal/oidc/provider/manager/manager.go | 2 +- 8 files changed, 1069 insertions(+), 214 deletions(-) create mode 100644 internal/oidc/login/post_login_handler_test.go diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 5f74b8bd..0c3df1e8 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -7,24 +7,21 @@ package auth import ( "fmt" "net/http" - "net/url" "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" - "github.com/felixge/httpsnoop" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/token/jwt" - "github.com/pkg/errors" "golang.org/x/oauth2" supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc" - "go.pinniped.dev/internal/authenticators" "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/login" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" @@ -127,36 +124,19 @@ func handleAuthRequestForLDAPUpstreamCLIFlow( return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication") } if !authenticated { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."), true) + return nil } - subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) + subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) username = authenticateResponse.User.GetName() groups := authenticateResponse.User.GetGroups() - dn := authenticateResponse.DN + customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse) + openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) + oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) - customSessionData := &psession.CustomSessionData{ - ProviderUID: ldapUpstream.GetResourceUID(), - ProviderName: ldapUpstream.GetName(), - ProviderType: idpType, - } - - if idpType == psession.ProviderTypeLDAP { - customSessionData.LDAP = &psession.LDAPSessionData{ - UserDN: dn, - ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, - } - } - if idpType == psession.ProviderTypeActiveDirectory { - customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{ - UserDN: dn, - ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, - } - } - - return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, - oauthHelper, authorizeRequester, subject, username, groups, customSessionData) + return nil } func handleAuthRequestForLDAPUpstreamBrowserFlow( @@ -191,20 +171,7 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( return nil } - loginURL, err := url.Parse(downstreamIssuer + "/login") - if err != nil { - return err - } - q := loginURL.Query() - q.Set("state", encodedStateParamValue) - loginURL.RawQuery = q.Encode() - - http.Redirect(w, r, - loginURL.String(), - http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 - ) - - return nil + return login.RedirectToLoginPage(r, w, downstreamIssuer, encodedStateParamValue, login.ShowNoError) } func handleAuthRequestForOIDCUpstreamPasswordGrant( @@ -225,9 +192,10 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( if !oidcUpstream.AllowsPasswordGrant() { // Return a user-friendly error for this case which is entirely within our control. - return writeAuthorizeError(w, oauthHelper, authorizeRequester, + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHint( "Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), true) + return nil } token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password) @@ -239,26 +207,33 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( // However, the exact response is undefined in the sense that there is no such thing as a password grant in // the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the // spec and returns something other than an "invalid_grant" error for bad resource owner credentials.) - return writeAuthorizeError(w, oauthHelper, authorizeRequester, + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithDebug(err.Error()), true) // WithDebug hides the error from the client + return nil } subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) if err != nil { // Return a user-friendly error for this case which is entirely within our control. - return writeAuthorizeError(w, oauthHelper, authorizeRequester, + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, ) + return nil } customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token) if err != nil { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, ) + return nil } - return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData) + openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) + + oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true) + + return nil } func handleAuthRequestForOIDCUpstreamAuthcodeGrant( @@ -322,78 +297,11 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant( return nil } -func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) error { - if plog.Enabled(plog.LevelTrace) { - // When trace level logging is enabled, include the stack trace in the log message. - keysAndValues := oidc.FositeErrorForLog(err) - errWithStack := errors.WithStack(err) - keysAndValues = append(keysAndValues, "errWithStack") - // klog always prints error values using %s, which does not include stack traces, - // so convert the error to a string which includes the stack trace here. - keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack)) - plog.Trace("authorize response error", keysAndValues...) - } else { - plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) - } - if isBrowserless { - w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w) - } - // Return an error according to OIDC spec 3.1.2.6 (second paragraph). - oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) - return nil -} - -func makeDownstreamSessionAndReturnAuthcodeRedirect( - r *http.Request, - w http.ResponseWriter, - oauthHelper fosite.OAuth2Provider, - authorizeRequester fosite.AuthorizeRequester, - subject string, - username string, - groups []string, - customSessionData *psession.CustomSessionData, -) error { - openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) - - authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) - if err != nil { - return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, true) - } - - w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w) - oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder) - - return nil -} - -func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter { - // rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs. - // we can drop this in a few releases once we feel enough time has passed for users to update. - // - // WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until - // https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address - // https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 - // Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect. - // - // in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's - // password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther. - return httpsnoop.Wrap(w, httpsnoop.Hooks{ - WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { - return func(code int) { - if code == http.StatusSeeOther { - code = http.StatusFound - } - delegate(code) - } - }, - }) -} - func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) { username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName) password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName) if username == "" || password == "" { - _ = writeAuthorizeError(w, oauthHelper, authorizeRequester, + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true) return "", "", false } @@ -403,7 +311,7 @@ func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseW func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, isBrowserless bool) (fosite.AuthorizeRequester, bool) { authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r) if err != nil { - _ = writeAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless) + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless) return nil, false } @@ -435,7 +343,8 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { return csrfFromCookie } -// Select either an OIDC, an LDAP or an AD IDP, or return an error. +// chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error. +// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values. func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) { oidcUpstreams := idpLister.GetOIDCIdentityProviders() ldapUpstreams := idpLister.GetLDAPIdentityProviders() @@ -503,7 +412,8 @@ func handleBrowserAuthRequest( }, }) if err != nil { - return "", "", "", writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false) + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, false) + return "", "", "", nil } csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) @@ -532,7 +442,8 @@ func handleBrowserAuthRequest( promptParam := r.Form.Get(promptParamName) if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { - return "", "", "", writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) + oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) + return "", "", "", nil } if csrfFromCookie == "" { @@ -608,8 +519,3 @@ func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken return nil } - -func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string { - ldapURL := *ldapUpstream.GetURL() - return downstreamsession.DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL) -} diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 128d5d4f..fc0cbc53 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -70,6 +70,8 @@ func TestAuthorizationEndpoint(t *testing.T) { downstreamClientID = "pinniped-cli" upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev" htmlContentType = "text/html; charset=utf-8" + jsonContentType = "application/json; charset=utf-8" + formContentType = "application/x-www-form-urlencoded" ) require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case") @@ -718,7 +720,7 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodPost, path: "/some/path", - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: "", @@ -737,7 +739,7 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodPost, path: "/some/path", - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: "", @@ -756,7 +758,7 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodPost, path: "/some/path", - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: "", @@ -770,7 +772,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), @@ -794,7 +796,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodPost, path: "/some/path", - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), @@ -817,7 +819,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), method: http.MethodPost, path: "/some/path", - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), @@ -845,7 +847,7 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -864,7 +866,7 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, @@ -883,10 +885,10 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}), - contentType: "application/x-www-form-urlencoded", + contentType: formContentType, body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery), wantBodyString: "", }, @@ -1113,7 +1115,7 @@ func TestAuthorizationEndpoint(t *testing.T) { Password: "wrong-password", }}, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedErrorQuery), wantBodyString: "", }, @@ -1125,7 +1127,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr("wrong-password"), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1137,7 +1139,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr("wrong-password"), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1149,7 +1151,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr("wrong-username"), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1161,7 +1163,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr("wrong-username"), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1173,7 +1175,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: nil, // do not send header customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1185,7 +1187,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: nil, // do not send header customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1197,7 +1199,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: nil, // do not send header customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1209,7 +1211,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1221,7 +1223,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1234,7 +1236,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery), wantBodyString: "", }, @@ -1247,7 +1249,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery), wantBodyString: "", }, @@ -1260,7 +1262,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantBodyString: "", }, @@ -1273,7 +1275,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantBodyString: "", }, @@ -1286,7 +1288,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantBodyString: "", }, @@ -1299,7 +1301,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantBodyString: "", }, @@ -1311,7 +1313,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, @@ -1323,7 +1325,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery), wantBodyString: "", }, @@ -1340,7 +1342,7 @@ func TestAuthorizationEndpoint(t *testing.T) { "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", }), wantStatus: http.StatusBadRequest, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, { @@ -1353,7 +1355,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusBadRequest, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, { @@ -1366,7 +1368,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusBadRequest, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, { @@ -1379,7 +1381,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusBadRequest, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, { @@ -1393,7 +1395,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1404,7 +1406,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1413,7 +1415,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1422,7 +1424,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1436,7 +1438,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, @@ -1448,7 +1450,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, @@ -1460,7 +1462,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, @@ -1470,7 +1472,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, @@ -1482,7 +1484,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, @@ -1492,7 +1494,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, @@ -1507,7 +1509,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, @@ -1519,7 +1521,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, @@ -1531,7 +1533,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, @@ -1543,7 +1545,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, @@ -1558,7 +1560,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, @@ -1570,7 +1572,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, @@ -1582,7 +1584,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, @@ -1592,7 +1594,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, @@ -1604,7 +1606,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, @@ -1614,7 +1616,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, @@ -1629,7 +1631,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1640,7 +1642,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1649,7 +1651,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), wantStatus: http.StatusUnauthorized, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantBodyJSON: fositeInvalidClientErrorBody, }, { @@ -1663,7 +1665,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantBodyString: "", }, @@ -1676,7 +1678,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1689,7 +1691,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1705,7 +1707,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantBodyString: "", }, @@ -1718,7 +1720,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1731,7 +1733,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1747,7 +1749,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", }, @@ -1760,7 +1762,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1773,7 +1775,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1789,7 +1791,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", }, @@ -1802,7 +1804,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1815,7 +1817,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error @@ -1833,7 +1835,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantBodyString: "", }, @@ -1848,7 +1850,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error @@ -1863,7 +1865,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantBodyString: "", wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error @@ -2052,7 +2054,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithInvalidEmailVerifiedHintErrorQuery), wantBodyString: "", }, @@ -2070,7 +2072,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithFalseEmailVerifiedHintErrorQuery), wantBodyString: "", }, @@ -2159,7 +2161,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery), wantBodyString: "", }, @@ -2198,7 +2200,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantBodyString: "", }, @@ -2213,7 +2215,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery), wantBodyString: "", }, @@ -2228,7 +2230,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery), wantBodyString: "", }, @@ -2243,7 +2245,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery), wantBodyString: "", }, @@ -2258,7 +2260,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantBodyString: "", }, @@ -2273,7 +2275,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery), wantBodyString: "", }, @@ -2288,7 +2290,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery), wantBodyString: "", }, @@ -2303,7 +2305,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantBodyString: "", }, @@ -2318,7 +2320,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantBodyString: "", }, @@ -2333,7 +2335,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantBodyString: "", }, @@ -2348,7 +2350,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantBodyString: "", }, @@ -2363,7 +2365,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}), wantStatus: http.StatusSeeOther, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantBodyString: "", }, @@ -2375,7 +2377,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantBodyString: "", }, @@ -2387,7 +2389,7 @@ func TestAuthorizationEndpoint(t *testing.T) { customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, - wantContentType: "application/json; charset=utf-8", + wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantBodyString: "", }, diff --git a/internal/oidc/downstreamsession/downstream_session.go b/internal/oidc/downstreamsession/downstream_session.go index 195aae00..2343c833 100644 --- a/internal/oidc/downstreamsession/downstream_session.go +++ b/internal/oidc/downstreamsession/downstream_session.go @@ -16,6 +16,7 @@ import ( "github.com/ory/fosite/token/jwt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/provider" @@ -61,6 +62,34 @@ func MakeDownstreamSession(subject string, username string, groups []string, cus return openIDSession } +func MakeDownstreamLDAPOrADCustomSessionData( + ldapUpstream provider.UpstreamLDAPIdentityProviderI, + idpType psession.ProviderType, + authenticateResponse *authenticators.Response, +) *psession.CustomSessionData { + customSessionData := &psession.CustomSessionData{ + ProviderUID: ldapUpstream.GetResourceUID(), + ProviderName: ldapUpstream.GetName(), + ProviderType: idpType, + } + + if idpType == psession.ProviderTypeLDAP { + customSessionData.LDAP = &psession.LDAPSessionData{ + UserDN: authenticateResponse.DN, + ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, + } + } + + if idpType == psession.ProviderTypeActiveDirectory { + customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{ + UserDN: authenticateResponse.DN, + ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, + } + } + + return customSessionData +} + func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token) (*psession.CustomSessionData, error) { upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, oidcUpstream.GetName(), token.IDToken.Claims) if err != nil { @@ -228,6 +257,11 @@ func ExtractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl return valueAsString, nil } +func DownstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string { + ldapURL := *ldapUpstream.GetURL() + return DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL) +} + func DownstreamLDAPSubject(uid string, ldapURL url.URL) string { q := ldapURL.Query() q.Set(oidc.IDTokenSubjectClaim, uid) diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go index a8e65e0e..751dc9c4 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/oidc/login/login_handler.go @@ -5,6 +5,7 @@ package login import ( "net/http" + "net/url" idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" "go.pinniped.dev/internal/httputil/httperr" @@ -13,6 +14,19 @@ import ( "go.pinniped.dev/internal/plog" ) +type ErrorParamValue string + +const ( + usernameParamName = "username" + passwordParamName = "password" + stateParamName = "state" + errParamName = "err" + + ShowNoError ErrorParamValue = "" + ShowInternalError ErrorParamValue = "internal_error" + ShowBadUserPassErr ErrorParamValue = "login_error" +) + // HandlerFunc is a function that can handle either a GET or POST request for the login endpoint. type HandlerFunc func( w http.ResponseWriter, @@ -66,3 +80,30 @@ func NewHandler( return securityheader.Wrap(loginHandler) } + +func RedirectToLoginPage( + r *http.Request, + w http.ResponseWriter, + downstreamIssuer string, + encodedStateParamValue string, + errToDisplay ErrorParamValue, +) error { + loginURL, err := url.Parse(downstreamIssuer + oidc.PinnipedLoginPath) + if err != nil { + return err + } + + q := loginURL.Query() + q.Set(stateParamName, encodedStateParamValue) + if errToDisplay != ShowNoError { + q.Set(errParamName, string(errToDisplay)) + } + loginURL.RawQuery = q.Encode() + + http.Redirect(w, r, + loginURL.String(), + http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 + ) + + return nil +} diff --git a/internal/oidc/login/post_login_handler.go b/internal/oidc/login/post_login_handler.go index 33819c69..5eb3a2e0 100644 --- a/internal/oidc/login/post_login_handler.go +++ b/internal/oidc/login/post_login_handler.go @@ -5,15 +5,84 @@ package login import ( "net/http" + "net/url" "github.com/ory/fosite" + "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/downstreamsession" + "go.pinniped.dev/internal/plog" ) -func NewPostHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc { +func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { - // TODO + // Note that the login handler prevents this handler from being called with OIDC upstreams. + _, ldapUpstream, idpType, err := oidc.FindUpstreamIDPByNameAndType(upstreamIDPs, decodedState.UpstreamName, decodedState.UpstreamType) + if err != nil { + // This shouldn't normally happen because the authorization endpoint ensured that this provider existed + // at that time. It would be possible in the unlikely event that the provider was deleted during the login. + plog.Error("error finding upstream provider", err) + return httperr.Wrap(http.StatusUnprocessableEntity, "error finding upstream provider", err) + } + + // Get the original params that were used at the authorization endpoint. + downstreamAuthParams, err := url.ParseQuery(decodedState.AuthParams) + if err != nil { + // This shouldn't really happen because the authorization endpoint encoded these query params correctly. + plog.Error("error reading state downstream auth params", err) + return httperr.New(http.StatusBadRequest, "error reading state downstream auth params") + } + + // Recreate enough of the original authorize request so we can pass it to NewAuthorizeRequest(). + reconstitutedAuthRequest := &http.Request{Form: downstreamAuthParams} + authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest) + if err != nil { + // This shouldn't really happen because the authorization endpoint has already validated these params + // by calling NewAuthorizeRequest() itself. + plog.Error("error using state downstream auth params", err) + return httperr.New(http.StatusBadRequest, "error using state downstream auth params") + } + + // Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested. + downstreamsession.GrantScopesIfRequested(authorizeRequester) + + // Get the username and password form params from the POST body. + username := r.PostFormValue(usernameParamName) + password := r.PostFormValue(passwordParamName) + + // Treat blank username or password as a bad username/password combination, as opposed to an internal error. + if username == "" || password == "" { + // User forgot to enter one of the required fields. + // The user may try to log in again if they'd like, so redirect back to the login page with an error. + return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr) + } + + // Attempt to authenticate the user with the upstream IDP. + authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password) + if err != nil { + plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) + // There was some problem during authentication with the upstream, aside from bad username/password. + // The user may try to log in again if they'd like, so redirect back to the login page with an error. + return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowInternalError) + } + if !authenticated { + // The upstream did not accept the username/password combination. + // The user may try to log in again if they'd like, so redirect back to the login page with an error. + return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr) + } + + // We had previously interrupted the regular steps of the OIDC authcode flow to show the login page UI. + // Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps. + // Both success and error responses from this point onwards should look like the usual fosite redirect + // responses, and a happy redirect response will include a downstream authcode. + subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) + username = authenticateResponse.User.GetName() + groups := authenticateResponse.User.GetGroups() + customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse) + openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData) + oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false) + return nil } } diff --git a/internal/oidc/login/post_login_handler_test.go b/internal/oidc/login/post_login_handler_test.go new file mode 100644 index 00000000..1e4fa437 --- /dev/null +++ b/internal/oidc/login/post_login_handler_test.go @@ -0,0 +1,693 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/kubernetes/fake" + + "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" +) + +func TestPostLoginEndpoint(t *testing.T) { + const ( + htmlContentType = "text/html; charset=utf-8" + + happyDownstreamCSRF = "test-csrf" + happyDownstreamPKCE = "test-pkce" + happyDownstreamNonce = "test-nonce" + happyDownstreamStateVersion = "2" + happyEncodedUpstreamState = "fake-encoded-state-param-value" + + downstreamIssuer = "https://my-downstream-issuer.com/path" + downstreamRedirectURI = "http://127.0.0.1/callback" + downstreamClientID = "pinniped-cli" + happyDownstreamState = "8b-state" + downstreamNonce = "some-nonce-value" + downstreamPKCEChallenge = "some-challenge" + downstreamPKCEChallengeMethod = "S256" + + ldapUpstreamName = "some-ldap-idp" + ldapUpstreamType = "ldap" + ldapUpstreamResourceUID = "ldap-resource-uid" + activeDirectoryUpstreamName = "some-active-directory-idp" + activeDirectoryUpstreamType = "activedirectory" + activeDirectoryUpstreamResourceUID = "active-directory-resource-uid" + upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev" + + userParam = "username" + passParam = "password" + badUserPassErrParamValue = "login_error" + internalErrParamValue = "internal_error" + ) + + var ( + fositeMissingCodeChallengeErrorQuery = map[string]string{ + "error": "invalid_request", + "error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must include a code_challenge when performing the authorize code flow, but it is missing.", + "state": happyDownstreamState, + } + + fositeInvalidCodeChallengeErrorQuery = map[string]string{ + "error": "invalid_request", + "error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The code_challenge_method is not supported, use S256 instead.", + "state": happyDownstreamState, + } + + fositeMissingCodeChallengeMethodErrorQuery = map[string]string{ + "error": "invalid_request", + "error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must use code_challenge_method=S256, plain is not allowed.", + "state": happyDownstreamState, + } + + fositePromptHasNoneAndOtherValueErrorQuery = map[string]string{ + "error": "invalid_request", + "error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Parameter 'prompt' was set to 'none', but contains other values as well which is not allowed.", + "state": happyDownstreamState, + } + ) + + happyDownstreamScopesRequested := []string{"openid"} + happyDownstreamScopesGranted := []string{"openid"} + + happyDownstreamRequestParamsQuery := url.Values{ + "response_type": []string{"code"}, + "scope": []string{strings.Join(happyDownstreamScopesRequested, " ")}, + "client_id": []string{downstreamClientID}, + "state": []string{happyDownstreamState}, + "nonce": []string{downstreamNonce}, + "code_challenge": []string{downstreamPKCEChallenge}, + "code_challenge_method": []string{downstreamPKCEChallengeMethod}, + "redirect_uri": []string{downstreamRedirectURI}, + } + happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode() + + copyOfHappyDownstreamRequestParamsQuery := func() url.Values { + params := url.Values{} + for k, v := range happyDownstreamRequestParamsQuery { + params[k] = make([]string, len(v)) + copy(params[k], v) + } + return params + } + + happyLDAPDecodedState := &oidc.UpstreamStateParamData{ + AuthParams: happyDownstreamRequestParams, + UpstreamName: ldapUpstreamName, + UpstreamType: ldapUpstreamType, + Nonce: happyDownstreamNonce, + CSRFToken: happyDownstreamCSRF, + PKCECode: happyDownstreamPKCE, + FormatVersion: happyDownstreamStateVersion, + } + + modifyHappyLDAPDecodedState := func(edit func(*oidc.UpstreamStateParamData)) *oidc.UpstreamStateParamData { + copyOfHappyLDAPDecodedState := *happyLDAPDecodedState + edit(©OfHappyLDAPDecodedState) + return ©OfHappyLDAPDecodedState + } + + happyActiveDirectoryDecodedState := &oidc.UpstreamStateParamData{ + AuthParams: happyDownstreamRequestParams, + UpstreamName: activeDirectoryUpstreamName, + UpstreamType: activeDirectoryUpstreamType, + Nonce: happyDownstreamNonce, + CSRFToken: happyDownstreamCSRF, + PKCECode: happyDownstreamPKCE, + FormatVersion: happyDownstreamStateVersion, + } + + happyLDAPUsername := "some-ldap-user" + happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username" + happyLDAPPassword := "some-ldap-password" //nolint:gosec + happyLDAPUID := "some-ldap-uid" + happyLDAPUserDN := "cn=foo,dn=bar" + happyLDAPGroups := []string{"group1", "group2", "group3"} + happyLDAPExtraRefreshAttribute := "some-refresh-attribute" + happyLDAPExtraRefreshValue := "some-refresh-attribute-value" + + parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL) + require.NoError(t, err) + + ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { + if username == "" || password == "" { + return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator") + } + if username == happyLDAPUsername && password == happyLDAPPassword { + return &authenticators.Response{ + User: &user.DefaultInfo{ + Name: happyLDAPUsernameFromAuthenticator, + UID: happyLDAPUID, + Groups: happyLDAPGroups, + }, + DN: happyLDAPUserDN, + ExtraRefreshAttributes: map[string]string{ + happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue, + }, + }, true, nil + } + return nil, false, nil + } + + upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ + Name: ldapUpstreamName, + ResourceUID: ldapUpstreamResourceUID, + URL: parsedUpstreamLDAPURL, + AuthenticateFunc: ldapAuthenticateFunc, + } + + upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ + Name: activeDirectoryUpstreamName, + ResourceUID: activeDirectoryUpstreamResourceUID, + URL: parsedUpstreamLDAPURL, + AuthenticateFunc: ldapAuthenticateFunc, + } + + erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ + Name: ldapUpstreamName, + ResourceUID: ldapUpstreamResourceUID, + AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) { + return nil, false, fmt.Errorf("some ldap upstream auth error") + }, + } + + expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{ + ProviderUID: activeDirectoryUpstreamResourceUID, + ProviderName: activeDirectoryUpstreamName, + ProviderType: psession.ProviderTypeActiveDirectory, + OIDC: nil, + LDAP: nil, + ActiveDirectory: &psession.ActiveDirectorySessionData{ + UserDN: happyLDAPUserDN, + ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue}, + }, + } + + expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{ + ProviderUID: ldapUpstreamResourceUID, + ProviderName: ldapUpstreamName, + ProviderType: psession.ProviderTypeLDAP, + OIDC: nil, + LDAP: &psession.LDAPSessionData{ + UserDN: happyLDAPUserDN, + ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue}, + }, + ActiveDirectory: nil, + } + + // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it + happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState + + happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}} + + encodeQuery := func(query map[string]string) string { + values := url.Values{} + for k, v := range query { + values[k] = []string{v} + } + return values.Encode() + } + + urlWithQuery := func(baseURL string, query map[string]string) string { + urlToReturn := fmt.Sprintf("%s?%s", baseURL, encodeQuery(query)) + _, err := url.Parse(urlToReturn) + require.NoError(t, err, "urlWithQuery helper was used to create an illegal URL") + return urlToReturn + } + + tests := []struct { + name string + idps *oidctestutil.UpstreamIDPListerBuilder + decodedState *oidc.UpstreamStateParamData + formParams url.Values + reqURIQuery url.Values + + wantStatus int + wantContentType string + wantBodyString string + wantErr string + + // Assertion that the response should be a redirect to the login page with an error param. + wantRedirectToLoginPageError string + + // Assertions for when an authcode should be returned, i.e. the request was authenticated by an + // upstream LDAP or AD provider. + wantRedirectLocationRegexp string // for loose matching + wantRedirectLocationString string // for exact matching instead + wantDownstreamRedirectURI string + wantDownstreamGrantedScopes []string + wantDownstreamIDTokenSubject string + wantDownstreamIDTokenUsername string + wantDownstreamIDTokenGroups []string + wantDownstreamRequestedScopes []string + wantDownstreamPKCEChallenge string + wantDownstreamPKCEChallengeMethod string + wantDownstreamNonce string + wantDownstreamCustomSessionData *psession.CustomSessionData + + // Authorization requests for either a successful OIDC upstream or for an error with any upstream + // should never use Kube storage. There is only one exception to this rule, which is that certain + // OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session) + // is stored, so it is possible with an LDAP upstream to store objects and then return an error to + // the client anyway (which makes the stored objects useless, but oh well). + wantUnnecessaryStoredRecords int + }{ + { + name: "happy LDAP login", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one + WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, + }, + { + name: "happy AD login", + idps: oidctestutil.NewUpstreamIDPListerBuilder(). + WithLDAP(&erroringUpstreamLDAPIdentityProvider). + WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one + decodedState: happyActiveDirectoryDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, + }, + { + name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["redirect_uri"] = []string{"http://127.0.0.1:4242/callback"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback", + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, + }, + { + name: "happy LDAP login when there are additional allowed downstream requested scopes", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["scope"] = []string{"openid offline_access pinniped:request-audience"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, + }, + { + name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["scope"] = []string{"email"} + // The following prompt value is illegal when openid is requested, but note that openid is not requested. + query["prompt"] = []string{"none login"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState, // no scopes granted + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: []string{"email"}, // only email was requested + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: []string{}, // no scopes granted + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, + }, + { + name: "bad username LDAP login", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: url.Values{userParam: []string{"wrong!"}, passParam: []string{happyLDAPPassword}}, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectToLoginPageError: badUserPassErrParamValue, + }, + { + name: "bad password LDAP login", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{"wrong!"}}, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectToLoginPageError: badUserPassErrParamValue, + }, + { + name: "blank username LDAP login", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: url.Values{userParam: []string{""}, passParam: []string{happyLDAPPassword}}, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectToLoginPageError: badUserPassErrParamValue, + }, + { + name: "blank password LDAP login", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{""}}, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectToLoginPageError: badUserPassErrParamValue, + }, + { + name: "username and password sent as URI query params should be ignored since they are expected in form post body", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + reqURIQuery: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectToLoginPageError: badUserPassErrParamValue, + }, + { + name: "error during upstream LDAP authentication", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider), + decodedState: happyLDAPDecodedState, + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectToLoginPageError: internalErrParamValue, + }, + { + name: "downstream redirect uri does not match what is configured for client", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["redirect_uri"] = []string{"http://127.0.0.1/wrong_callback"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "downstream client does not exist", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["client_id"] = []string{"wrong_client_id"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "downstream client is missing", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + delete(query, "client_id") + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "response type is unsupported", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["response_type"] = []string{"unsupported"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "response type is missing", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + delete(query, "response_type") + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "PKCE code_challenge is missing", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + delete(query, "code_challenge") + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "PKCE code_challenge_method is invalid", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["code_challenge_method"] = []string{"this-is-not-a-valid-pkce-alg"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "PKCE code_challenge_method is `plain`", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["code_challenge_method"] = []string{"plain"} // plain is not allowed + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "PKCE code_challenge_method is missing", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + delete(query, "code_challenge_method") + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "prompt param is not allowed to have none and another legal value at the same time", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["prompt"] = []string{"none login"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyString: "", + wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), + wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error + }, + { + name: "downstream state does not have enough entropy", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["state"] = []string{"short"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "downstream scopes do not match what is configured for client", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["scope"] = []string{"openid offline_access pinniped:request-audience scope_not_allowed"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantErr: "error using state downstream auth params", + }, + { + name: "no upstream providers are configured or provider cannot be found by name", + idps: oidctestutil.NewUpstreamIDPListerBuilder(), // empty + decodedState: happyLDAPDecodedState, + formParams: happyUsernamePasswordFormParams, + wantErr: "error finding upstream provider: provider not found", + }, + { + name: "upstream provider cannot be found by name and type", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: happyActiveDirectoryDecodedState, // correct upstream IDP name, but wrong upstream IDP type + formParams: happyUsernamePasswordFormParams, + wantErr: "error finding upstream provider: provider not found", + }, + } + + for _, test := range tests { + tt := test + + t.Run(tt.name, func(t *testing.T) { + kubeClient := fake.NewSimpleClientset() + secretsClient := kubeClient.CoreV1().Secrets("some-namespace") + + // Configure fosite the same way that the production code would. + // Inject this into our test subject at the last second so we get a fresh storage for every test. + timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() + kubeOauthStore := oidc.NewKubeStorage(secretsClient, timeoutsConfiguration) + hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } + require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") + jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() + oauthHelper := oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration) + + req := httptest.NewRequest(http.MethodPost, "/ignored", strings.NewReader(tt.formParams.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if tt.reqURIQuery != nil { + req.URL.RawQuery = tt.reqURIQuery.Encode() + } + + rsp := httptest.NewRecorder() + + subject := NewPostHandler(downstreamIssuer, tt.idps.Build(), oauthHelper) + + err := subject(rsp, req, happyEncodedUpstreamState, tt.decodedState) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Empty(t, kubeClient.Actions()) + return // the http response doesn't matter when the function returns an error, because the caller should handle the error + } + // Otherwise, expect no error. + require.NoError(t, err) + + require.Equal(t, tt.wantStatus, rsp.Code) + testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) + require.Equal(t, test.wantBodyString, rsp.Body.String()) + + actualLocation := rsp.Header().Get("Location") + + switch { + case tt.wantRedirectLocationRegexp != "": + require.Len(t, rsp.Header().Values("Location"), 1) + oidctestutil.RequireAuthCodeRegexpMatch( + t, + actualLocation, + test.wantRedirectLocationRegexp, + kubeClient, + secretsClient, + kubeOauthStore, + test.wantDownstreamGrantedScopes, + test.wantDownstreamIDTokenSubject, + test.wantDownstreamIDTokenUsername, + test.wantDownstreamIDTokenGroups, + test.wantDownstreamRequestedScopes, + test.wantDownstreamPKCEChallenge, + test.wantDownstreamPKCEChallengeMethod, + test.wantDownstreamNonce, + downstreamClientID, + test.wantDownstreamRedirectURI, + test.wantDownstreamCustomSessionData, + ) + case tt.wantRedirectToLoginPageError != "": + expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath + + "?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState + require.Equal(t, expectedLocation, actualLocation) + require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords) + case tt.wantRedirectLocationString != "": + require.Equal(t, tt.wantRedirectLocationString, actualLocation) + require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords) + default: + require.Failf(t, "test should have expected a redirect", + "actual location was %q", actualLocation) + } + }) + } +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 90c47655..b45e757a 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -6,18 +6,25 @@ package oidc import ( "crypto/subtle" + "errors" + "fmt" "net/http" "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/felixge/httpsnoop" "github.com/ory/fosite" "github.com/ory/fosite/compose" + errorsx "github.com/pkg/errors" + "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" "go.pinniped.dev/internal/httputil/httperr" "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/internal/plog" + "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" ) @@ -365,3 +372,106 @@ func validateCSRFValue(state *UpstreamStateParamData, csrfCookieValue csrftoken. } return nil } + +// FindUpstreamIDPByNameAndType finds the requested IDP by name and type, or returns an error. +// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values. +func FindUpstreamIDPByNameAndType( + idpLister UpstreamIdentityProvidersLister, + upstreamName string, + upstreamType string, +) ( + provider.UpstreamOIDCIdentityProviderI, + provider.UpstreamLDAPIdentityProviderI, + psession.ProviderType, + error, +) { + switch upstreamType { + case string(v1alpha1.IDPTypeOIDC): + for _, p := range idpLister.GetOIDCIdentityProviders() { + if p.GetName() == upstreamName { + return p, nil, psession.ProviderTypeOIDC, nil + } + } + case string(v1alpha1.IDPTypeLDAP): + for _, p := range idpLister.GetLDAPIdentityProviders() { + if p.GetName() == upstreamName { + return nil, p, psession.ProviderTypeLDAP, nil + } + } + case string(v1alpha1.IDPTypeActiveDirectory): + for _, p := range idpLister.GetActiveDirectoryIdentityProviders() { + if p.GetName() == upstreamName { + return nil, p, psession.ProviderTypeActiveDirectory, nil + } + } + } + return nil, nil, "", errors.New("provider not found") +} + +// WriteAuthorizeError writes an authorization error as it should be returned by the authorization endpoint and other +// similar endpoints that are the end of the downstream authcode flow. Errors responses are written in the usual fosite style. +func WriteAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) { + if plog.Enabled(plog.LevelTrace) { + // When trace level logging is enabled, include the stack trace in the log message. + keysAndValues := FositeErrorForLog(err) + errWithStack := errorsx.WithStack(err) + keysAndValues = append(keysAndValues, "errWithStack") + // klog always prints error values using %s, which does not include stack traces, + // so convert the error to a string which includes the stack trace here. + keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack)) + plog.Trace("authorize response error", keysAndValues...) + } else { + plog.Info("authorize response error", FositeErrorForLog(err)...) + } + if isBrowserless { + w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w) + } + // Return an error according to OIDC spec 3.1.2.6 (second paragraph). + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) +} + +// PerformAuthcodeRedirect successfully completes a downstream login by creating a session and +// writing the authcode redirect response as it should be returned by the authorization endpoint and other +// similar endpoints that are the end of the downstream authcode flow. +func PerformAuthcodeRedirect( + r *http.Request, + w http.ResponseWriter, + oauthHelper fosite.OAuth2Provider, + authorizeRequester fosite.AuthorizeRequester, + openIDSession *psession.PinnipedSession, + isBrowserless bool, +) { + authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) + if err != nil { + plog.WarningErr("error while generating and saving authcode", err) + WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless) + return + } + if isBrowserless { + w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w) + } + oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder) +} + +func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter { + // rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs. + // we can drop this in a few releases once we feel enough time has passed for users to update. + // + // WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until + // https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address + // https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 + // Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect. + // + // in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's + // password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther. + return httpsnoop.Wrap(w, httpsnoop.Hooks{ + WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { + return func(code int) { + if code == http.StatusSeeOther { + code = http.StatusFound + } + delegate(code) + } + }, + }) +} diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 283b1808..3da0c2c3 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -140,7 +140,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs upstreamStateEncoder, csrfCookieEncoder, login.NewGetHandler(m.upstreamIDPs), - login.NewPostHandler(m.upstreamIDPs, oauthHelperWithKubeStorage), + login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage), ) plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer) From dfbc33b933cdb5e43982c821fe5fbf65192b95be Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 2 May 2022 09:47:09 -0700 Subject: [PATCH 28/77] Apply suggestions from code review Co-authored-by: Mo Khan --- proposals/1141_audit-logging/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/1141_audit-logging/README.md b/proposals/1141_audit-logging/README.md index d6ae0341..bc64a476 100644 --- a/proposals/1141_audit-logging/README.md +++ b/proposals/1141_audit-logging/README.md @@ -25,7 +25,7 @@ logs. ### How Pinniped Works Today (as of version v0.16.0) The Pinniped Supervisor and Concierge components are Kubernetes Deployments. Today, each Pod has a single container, -which is the Supervisor or Concierge app. Kubernetes captures the stdout of the app into the Pod logs. +which is the Supervisor or Concierge app. Kubernetes captures the stdout and stderr of the app into the Pod logs. Today, the Pinniped Supervisor and Concierge log many interesting events to their Pod logs. These logs are meant primarily to help an admin user debug problems with their Pinniped configuration or with their cluster. The Supervisor @@ -59,7 +59,7 @@ Goals Non-goals -- Enabling auditing in the impersonation proxy. If needed, this will be handled in a separate feature. +- Enabling Kubernetes API request auditing in the impersonation proxy. If needed, this will be handled in a separate feature. - Providing the ability to filter or choose which audit events to capture. - Auditing the management of CRs (e.g. OIDCIdentityProvider). These events are captured by the API server audit logs. @@ -252,7 +252,7 @@ a GKE Ingress, if no health checks were configured. #### Tests -Audit logging will be a user-facing feature, and the format of the logs should be considered a kind of documented API. +Audit logging will be a user-facing feature, and the format of the logs should be considered a documented and versioned API. Unnecessary changes to the format should be avoided after the first release. Therefore, all audit log events should be covered by unit tests. From c74dea640599eff1cab7a93151e751d89657791c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 2 May 2022 13:37:32 -0700 Subject: [PATCH 29/77] Escape special characters in LDAP DNs when used in search filters --- internal/upstreamldap/upstreamldap.go | 17 +++-- internal/upstreamldap/upstreamldap_test.go | 88 +++++++++++++++++++++- 2 files changed, 98 insertions(+), 7 deletions(-) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 136fc023..ddee048b 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -715,7 +715,9 @@ func (p *Provider) groupSearchRequestedAttributes() []string { } func (p *Provider) userSearchFilter(username string) string { - safeUsername := p.escapeUsernameForSearchFilter(username) + // The username is end user input, so it should be escaped before being included in a search to prevent + // query injection. + safeUsername := p.escapeForSearchFilter(username) if len(p.c.UserSearch.Filter) == 0 { return fmt.Sprintf("(%s=%s)", p.c.UserSearch.UsernameAttribute, safeUsername) } @@ -723,10 +725,14 @@ func (p *Provider) userSearchFilter(username string) string { } func (p *Provider) groupSearchFilter(userDN string) string { + // The DN can contain characters that are considered special characters by LDAP searches, so it should be + // escaped before being included in the search filter to prevent bad search syntax. + // E.g. for the DN `CN=My User (Admin),OU=Users,OU=my,DC=my,DC=domain` we must escape the parens. + safeUserDN := p.escapeForSearchFilter(userDN) if len(p.c.GroupSearch.Filter) == 0 { - return fmt.Sprintf("(member=%s)", userDN) + return fmt.Sprintf("(member=%s)", safeUserDN) } - return interpolateSearchFilter(p.c.GroupSearch.Filter, userDN) + return interpolateSearchFilter(p.c.GroupSearch.Filter, safeUserDN) } func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) string { @@ -737,9 +743,8 @@ func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) return "(" + filter + ")" } -func (p *Provider) escapeUsernameForSearchFilter(username string) string { - // The username is end user input, so it should be escaped before being included in a search to prevent query injection. - return ldap.EscapeFilter(username) +func (p *Provider) escapeForSearchFilter(s string) string { + return ldap.EscapeFilter(s) } // Returns the (potentially) binary data of the attribute's value, base64 URL encoded. diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 311aa6ec..c8bdc395 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -479,7 +479,7 @@ func TestEndUserAuthentication(t *testing.T) { wantAuthResponse: expectedAuthResponse(nil), }, { - name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter, because the username is end-user input", + name: "when the username has special LDAP search filter characters then they must be properly escaped in the custom user search filter, because the username is end-user input", username: `a&b|c(d)e\f*g`, password: testUpstreamPassword, providerConfig: providerConfig(nil), @@ -497,6 +497,92 @@ func TestEndUserAuthentication(t *testing.T) { }, wantAuthResponse: expectedAuthResponse(nil), }, + { + name: "when the username has special LDAP search filter characters then they must be properly escaped in the default user search filter, because the username is end-user input", + username: `a&b|c(d)e\f*g`, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UserSearch.Filter = "" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Filter = fmt.Sprintf("(some-upstream-username-attribute=%s)", `a&b|c\28d\29e\5cf\2ag`) + })).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when the user search result DN has special LDAP search filter characters then they must be properly escaped in the custom group search filter", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: `result DN with * \ special characters ()`, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + escapedDN := `result DN with \2a \5c special characters \28\29` + r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", escapedDN, escapedDN) + }), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) { + r.DN = `result DN with * \ special characters ()` + }), + }, + { + name: "when the user search result DN has special LDAP search filter characters then they must be properly escaped in the default group search filter", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.Filter = "" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: `result DN with * \ special characters ()`, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Filter = fmt.Sprintf("(member=%s)", `result DN with \2a \5c special characters \28\29`) + }), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) { + r.DN = `result DN with * \ special characters ()` + }), + }, { name: "group names are sorted to make the result more stable/predictable", username: testUpstreamUsername, From 90e88bb83cc35fbded262b149095d0dc6cd82dcc Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 2 May 2022 14:33:33 -0700 Subject: [PATCH 30/77] Update kube codegen versions Note that attempting to update 1.18.18 to 1.18.20 didn't work for some reason, so I skipped that one. The code generator didn't like 1.18.20 and it deleted all the generated code. Avoiding 1.18.19 because it is listed as having a regression at https://kubernetes.io/releases/patch-releases/#non-active-branch-history --- generated/1.17/apis/go.mod | 4 ++-- generated/1.17/apis/go.sum | 8 ++++---- generated/1.17/client/go.mod | 4 ++-- generated/1.17/client/go.sum | 12 ++++++------ generated/1.19/apis/go.mod | 4 ++-- generated/1.19/apis/go.sum | 8 ++++---- generated/1.19/client/go.mod | 4 ++-- generated/1.19/client/go.sum | 12 ++++++------ generated/1.21/apis/go.mod | 4 ++-- generated/1.21/apis/go.sum | 8 ++++---- generated/1.21/client/go.mod | 4 ++-- generated/1.21/client/go.sum | 16 ++++++++-------- hack/lib/kube-versions.txt | 6 +++--- 13 files changed, 47 insertions(+), 47 deletions(-) diff --git a/generated/1.17/apis/go.mod b/generated/1.17/apis/go.mod index 1b5413f9..623dbe76 100644 --- a/generated/1.17/apis/go.mod +++ b/generated/1.17/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.17/apis go 1.13 require ( - k8s.io/api v0.17.16 - k8s.io/apimachinery v0.17.16 + k8s.io/api v0.17.17 + k8s.io/apimachinery v0.17.17 ) diff --git a/generated/1.17/apis/go.sum b/generated/1.17/apis/go.sum index 8b02fb72..fd58ed3a 100644 --- a/generated/1.17/apis/go.sum +++ b/generated/1.17/apis/go.sum @@ -91,10 +91,10 @@ 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.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -k8s.io/api v0.17.16 h1:whKfZJJp9m5fklRnlvO8+mJzpXat0gX0n+90d1hWTu0= -k8s.io/api v0.17.16/go.mod h1:W8uKRxJeYRlAbWuk4CZv6BzuC7KuZnB6bSTPI7Pi8no= -k8s.io/apimachinery v0.17.16 h1:A9HqHhUGgUNwki1c1lY6w773WMY1Qx/jR3r9baX1unQ= -k8s.io/apimachinery v0.17.16/go.mod h1:T54ZSpncArE25c5r2PbUPsLeTpkPWY/ivafigSX6+xk= +k8s.io/api v0.17.17 h1:S+Yv5pdfvy9OG1t148zMFk3/l/VYpF1N4j5Y/q8IMdg= +k8s.io/api v0.17.17/go.mod h1:kk4nQM0EVx+BEY7o8CN5YL99CWmWEQ2a4NCak58yB6E= +k8s.io/apimachinery v0.17.17 h1:HMpFl9yqNI5G2+2WllKOe2XYLkCyaWzfXvk7SosyVko= +k8s.io/apimachinery v0.17.17/go.mod h1:T54ZSpncArE25c5r2PbUPsLeTpkPWY/ivafigSX6+xk= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= diff --git a/generated/1.17/client/go.mod b/generated/1.17/client/go.mod index d648ab26..17693b27 100644 --- a/generated/1.17/client/go.mod +++ b/generated/1.17/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.17/apis v0.0.0 - k8s.io/apimachinery v0.17.16 - k8s.io/client-go v0.17.16 + k8s.io/apimachinery v0.17.17 + k8s.io/client-go v0.17.17 ) replace go.pinniped.dev/generated/1.17/apis => ../apis diff --git a/generated/1.17/client/go.sum b/generated/1.17/client/go.sum index e6a9ec1c..3206b9f6 100644 --- a/generated/1.17/client/go.sum +++ b/generated/1.17/client/go.sum @@ -176,12 +176,12 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.17.16 h1:whKfZJJp9m5fklRnlvO8+mJzpXat0gX0n+90d1hWTu0= -k8s.io/api v0.17.16/go.mod h1:W8uKRxJeYRlAbWuk4CZv6BzuC7KuZnB6bSTPI7Pi8no= -k8s.io/apimachinery v0.17.16 h1:A9HqHhUGgUNwki1c1lY6w773WMY1Qx/jR3r9baX1unQ= -k8s.io/apimachinery v0.17.16/go.mod h1:T54ZSpncArE25c5r2PbUPsLeTpkPWY/ivafigSX6+xk= -k8s.io/client-go v0.17.16 h1:5g4fmARsp1VFn8tSRZxYpk/DdrT4eGFF/ydYR1tW3VM= -k8s.io/client-go v0.17.16/go.mod h1:TwGfS07/0RyVp+PjSZEg9piBGveZ+hEg9zMUBg1Upbo= +k8s.io/api v0.17.17 h1:S+Yv5pdfvy9OG1t148zMFk3/l/VYpF1N4j5Y/q8IMdg= +k8s.io/api v0.17.17/go.mod h1:kk4nQM0EVx+BEY7o8CN5YL99CWmWEQ2a4NCak58yB6E= +k8s.io/apimachinery v0.17.17 h1:HMpFl9yqNI5G2+2WllKOe2XYLkCyaWzfXvk7SosyVko= +k8s.io/apimachinery v0.17.17/go.mod h1:T54ZSpncArE25c5r2PbUPsLeTpkPWY/ivafigSX6+xk= +k8s.io/client-go v0.17.17 h1:5jTDCwRXCKJwmPvtgTFgCSMIzdyAOUyPmSU3PHIuVVY= +k8s.io/client-go v0.17.17/go.mod h1:IpXd6i0FlhG3fJ+UuEWMfTUaDw6TlmMkpjmJrmbY6tY= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= diff --git a/generated/1.19/apis/go.mod b/generated/1.19/apis/go.mod index be417432..bcfdfd7a 100644 --- a/generated/1.19/apis/go.mod +++ b/generated/1.19/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.19/apis go 1.13 require ( - k8s.io/api v0.19.15 - k8s.io/apimachinery v0.19.15 + k8s.io/api v0.19.16 + k8s.io/apimachinery v0.19.16 ) diff --git a/generated/1.19/apis/go.sum b/generated/1.19/apis/go.sum index e65852db..4aef2b33 100644 --- a/generated/1.19/apis/go.sum +++ b/generated/1.19/apis/go.sum @@ -171,10 +171,10 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.19.15 h1:i22aQYrQ9gaBHEAS9XvyR5ZfrTDAd+Q+JwWM+xIBv30= -k8s.io/api v0.19.15/go.mod h1:rMRWjnIJQmurd/FdLobht6dCSbJQ+UDpyOwPaoFS7lI= -k8s.io/apimachinery v0.19.15 h1:P37ni6/yFxRMrqgM75k/vt5xq9vnNiR3rJPTmWXrNho= -k8s.io/apimachinery v0.19.15/go.mod h1:RMyblyny2ZcDQ/oVE+lC31u7XTHUaSXEK2IhgtwGxfc= +k8s.io/api v0.19.16 h1:Z6gEEaKkM6I24yY/VGkvZ4QFnqvfWk88w2I6oDODruE= +k8s.io/api v0.19.16/go.mod h1:Vz9ZfXbI/35CtXGfM4mUDPuTQw7dLeZY31EO0OohMSQ= +k8s.io/apimachinery v0.19.16 h1:9tPZlQtPlxqmjJKPoaW9+ABj9o4BcIB0emora+Tf2m8= +k8s.io/apimachinery v0.19.16/go.mod h1:RMyblyny2ZcDQ/oVE+lC31u7XTHUaSXEK2IhgtwGxfc= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= diff --git a/generated/1.19/client/go.mod b/generated/1.19/client/go.mod index 3ffb69b7..3e108c3a 100644 --- a/generated/1.19/client/go.mod +++ b/generated/1.19/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.19/apis v0.0.0 - k8s.io/apimachinery v0.19.15 - k8s.io/client-go v0.19.15 + k8s.io/apimachinery v0.19.16 + k8s.io/client-go v0.19.16 ) replace go.pinniped.dev/generated/1.19/apis => ../apis diff --git a/generated/1.19/client/go.sum b/generated/1.19/client/go.sum index cf7abe3c..562357fe 100644 --- a/generated/1.19/client/go.sum +++ b/generated/1.19/client/go.sum @@ -320,12 +320,12 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.19.15 h1:i22aQYrQ9gaBHEAS9XvyR5ZfrTDAd+Q+JwWM+xIBv30= -k8s.io/api v0.19.15/go.mod h1:rMRWjnIJQmurd/FdLobht6dCSbJQ+UDpyOwPaoFS7lI= -k8s.io/apimachinery v0.19.15 h1:P37ni6/yFxRMrqgM75k/vt5xq9vnNiR3rJPTmWXrNho= -k8s.io/apimachinery v0.19.15/go.mod h1:RMyblyny2ZcDQ/oVE+lC31u7XTHUaSXEK2IhgtwGxfc= -k8s.io/client-go v0.19.15 h1:lDBvFBjDIExh0mFS6JbG+5B7ghuPhqXjBzlaxG81ToU= -k8s.io/client-go v0.19.15/go.mod h1:OJMQWgHQJRDtO2BVtpkHUQOq/e5WHpXc02lSdPI0S/k= +k8s.io/api v0.19.16 h1:Z6gEEaKkM6I24yY/VGkvZ4QFnqvfWk88w2I6oDODruE= +k8s.io/api v0.19.16/go.mod h1:Vz9ZfXbI/35CtXGfM4mUDPuTQw7dLeZY31EO0OohMSQ= +k8s.io/apimachinery v0.19.16 h1:9tPZlQtPlxqmjJKPoaW9+ABj9o4BcIB0emora+Tf2m8= +k8s.io/apimachinery v0.19.16/go.mod h1:RMyblyny2ZcDQ/oVE+lC31u7XTHUaSXEK2IhgtwGxfc= +k8s.io/client-go v0.19.16 h1:DM3Rb3vdhgKAQeZ9U5hU467wt9qPX8ogqMCu2qYC/Wc= +k8s.io/client-go v0.19.16/go.mod h1:aEi/M7URDBWUIzdFt/l/WkngaqCTYtDo0cIMIQgvXmI= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= diff --git a/generated/1.21/apis/go.mod b/generated/1.21/apis/go.mod index 8db9293f..916eef22 100644 --- a/generated/1.21/apis/go.mod +++ b/generated/1.21/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.21/apis go 1.13 require ( - k8s.io/api v0.21.10 - k8s.io/apimachinery v0.21.10 + k8s.io/api v0.21.11 + k8s.io/apimachinery v0.21.11 ) diff --git a/generated/1.21/apis/go.sum b/generated/1.21/apis/go.sum index d1e7b7d0..45e90b8e 100644 --- a/generated/1.21/apis/go.sum +++ b/generated/1.21/apis/go.sum @@ -147,10 +147,10 @@ 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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.21.10 h1:WKcYyNBZNMrE9yejBs0Lx70jGsOW8uUwkiA4ioxkz1Q= -k8s.io/api v0.21.10/go.mod h1:5kqv2pCXwcrOvV12WhVAtLZUKaM0kyrZ6nHObw8SojA= -k8s.io/apimachinery v0.21.10 h1:mOStSZoCrsxnAMIm5UtCNn6P328cJAhtzJToQYFsylc= -k8s.io/apimachinery v0.21.10/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= +k8s.io/api v0.21.11 h1:wJYhJfpvLkOJb+KdfLb2ps8gb+gPNkyLnevt4Yyssd4= +k8s.io/api v0.21.11/go.mod h1:ipplJOizdDZsizpXHt1uek+yMsoclq2so9ks2aG7yqA= +k8s.io/apimachinery v0.21.11 h1:oi/sFpeUWJIhxrUe4Kn1cwxAGJ0WJ3AQNz5bmeV6klI= +k8s.io/apimachinery v0.21.11/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.21/client/go.mod b/generated/1.21/client/go.mod index 4222b11d..23cde429 100644 --- a/generated/1.21/client/go.mod +++ b/generated/1.21/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.21/apis v0.0.0 - k8s.io/apimachinery v0.21.10 - k8s.io/client-go v0.21.10 + k8s.io/apimachinery v0.21.11 + k8s.io/client-go v0.21.11 ) replace go.pinniped.dev/generated/1.21/apis => ../apis diff --git a/generated/1.21/client/go.sum b/generated/1.21/client/go.sum index cf649bbd..c515ca46 100644 --- a/generated/1.21/client/go.sum +++ b/generated/1.21/client/go.sum @@ -402,20 +402,20 @@ 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= -k8s.io/api v0.21.10 h1:WKcYyNBZNMrE9yejBs0Lx70jGsOW8uUwkiA4ioxkz1Q= -k8s.io/api v0.21.10/go.mod h1:5kqv2pCXwcrOvV12WhVAtLZUKaM0kyrZ6nHObw8SojA= -k8s.io/apimachinery v0.21.10 h1:mOStSZoCrsxnAMIm5UtCNn6P328cJAhtzJToQYFsylc= -k8s.io/apimachinery v0.21.10/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= -k8s.io/client-go v0.21.10 h1:/AKJEgLpQDWvZbq7cq2vEx0bpqpAlOOHitOrctSV8bI= -k8s.io/client-go v0.21.10/go.mod h1:nAGhVCjwhbDP2whk65n3STSCn24H/VGp1pKSk9UszU8= +k8s.io/api v0.21.11 h1:wJYhJfpvLkOJb+KdfLb2ps8gb+gPNkyLnevt4Yyssd4= +k8s.io/api v0.21.11/go.mod h1:ipplJOizdDZsizpXHt1uek+yMsoclq2so9ks2aG7yqA= +k8s.io/apimachinery v0.21.11 h1:oi/sFpeUWJIhxrUe4Kn1cwxAGJ0WJ3AQNz5bmeV6klI= +k8s.io/apimachinery v0.21.11/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= +k8s.io/client-go v0.21.11 h1:AIA8Yq/pTx+wyB/x3OYrmYJJCdcC7YPOrmwnW0Ws3Yk= +k8s.io/client-go v0.21.11/go.mod h1:VYCT1Xg3OkLEN/O2zY4qLiLekWg1m/TEEw0wsZ0OlX0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 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-openapi v0.0.0-20211110012726-3cc51fd1e909 h1:s77MRc/+/eQjsF89MB12JssAlsoi9mnNoaacRqibeAU= k8s.io/kube-openapi v0.0.0-20211110012726-3cc51fd1e909/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/utils v0.0.0-20210521133846-da695404a2bc h1:dx6VGe+PnOW/kD/2UV4aUSsRfJGd7+lcqgJ6Xg0HwUs= -k8s.io/utils v0.0.0-20210521133846-da695404a2bc/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/hack/lib/kube-versions.txt b/hack/lib/kube-versions.txt index 62edf1ff..b8815915 100644 --- a/hack/lib/kube-versions.txt +++ b/hack/lib/kube-versions.txt @@ -1,7 +1,7 @@ 1.23.5 1.22.8 -1.21.10 +1.21.11 1.20.15 -1.19.15 +1.19.16 1.18.18 -1.17.16 +1.17.17 From eaa87c762898951ccc399bf6dde42913c4781f95 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 3 May 2022 12:59:39 -0700 Subject: [PATCH 31/77] support AD in hack/prepare-supervisor-on-kind.sh --- hack/prepare-supervisor-on-kind.sh | 78 +++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 8b6d5969..9c17d26c 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -1,12 +1,17 @@ #!/usr/bin/env bash -# Copyright 2021 the Pinniped contributors. All Rights Reserved. +# Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # # A script to perform the setup required to manually test using the supervisor on a kind cluster. # Assumes that you installed the apps already using hack/prepare-for-integration-tests.sh. # +# This script is a little hacky to avoid setting up any kind of ingress or load balancer on Kind. +# It uses an http proxy server and port forwarding to route the requests into the cluster. +# This is only intended for quick manual testing of features by contributors and is not a +# representation of how to really deploy or configure Pinniped. +# # This uses the Supervisor and Concierge in the same cluster. Usually the Supervisor would be # deployed in one cluster while each workload cluster would have a Concierge. All the workload # cluster Concierge configurations would be similar to each other, all trusting the same Supervisor. @@ -22,6 +27,7 @@ cd "$ROOT" use_oidc_upstream=no use_ldap_upstream=no +use_ad_upstream=no while (("$#")); do case "$1" in --ldap) @@ -32,6 +38,12 @@ while (("$#")); do use_oidc_upstream=yes shift ;; + --ad) + # Use an ActiveDirectoryIdentityProvider. + # This assumes that you used the --get-active-directory-vars flag with hack/prepare-for-integration-tests.sh. + use_ad_upstream=yes + shift + ;; -*) log_error "Unsupported flag $1" >&2 exit 1 @@ -43,8 +55,8 @@ while (("$#")); do esac done -if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" ]]; then - echo "Error: Please use --oidc or --ldap to specify which type of upstream identity provider(s) you would like" +if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" ]]; then + echo "Error: Please use --oidc, --ldap, or --ad to specify which type of upstream identity provider(s) you would like" exit 1 fi @@ -95,6 +107,7 @@ spec: EOF echo "Waiting for FederationDomain to initialize..." +# Sleeping is a race, but that's probably good enough for the purposes of this script. sleep 5 # Test that the federation domain is working before we proceed. @@ -152,6 +165,10 @@ spec: certificateAuthorityData: "$PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE" bind: secretName: my-ldap-service-account + groupSearch: + base: "$PINNIPED_TEST_LDAP_GROUPS_SEARCH_BASE" + attributes: + groupName: "cn" userSearch: base: "$PINNIPED_TEST_LDAP_USERS_SEARCH_BASE" filter: "cn={}" @@ -178,6 +195,39 @@ EOF --dry-run=client --output yaml | kubectl apply -f - fi +if [[ "$use_ad_upstream" == "yes" ]]; then + # Make an ActiveDirectoryIdentityProvider. + cat <kubeconfig @@ -224,14 +281,21 @@ if [[ "$use_ldap_upstream" == "yes" ]]; then echo " Password: $PINNIPED_TEST_LDAP_USER_PASSWORD" fi +if [[ "$use_ad_upstream" == "yes" ]]; then + echo + echo "When prompted for username and password by the CLI, use these values:" + echo " Username: $PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME" + echo " Password: $PINNIPED_TEST_AD_USER_PASSWORD" +fi + # Perform a login using the kubectl plugin. This should print the URL to be followed for the Dex login page # if using an OIDC upstream, or should prompt on the CLI for username/password if using an LDAP upstream. echo -echo "Running: https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" kubectl --kubeconfig ./kubeconfig get pods -A" -https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" kubectl --kubeconfig ./kubeconfig get pods -A +echo "Running: PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" kubectl --kubeconfig ./kubeconfig get pods -A" +PINNIPED_DEBUG=true https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" kubectl --kubeconfig ./kubeconfig get pods -A # Print the identity of the currently logged in user. The CLI has cached your tokens, and will automatically refresh # your short-lived credentials whenever they expire, so you should not be prompted to log in again for the rest of the day. echo -echo "Running: https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" ./pinniped whoami --kubeconfig ./kubeconfig" -https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped whoami --kubeconfig ./kubeconfig +echo "Running: PINNIPED_DEBUG=true https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" ./pinniped whoami --kubeconfig ./kubeconfig" +PINNIPED_DEBUG=true https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped whoami --kubeconfig ./kubeconfig From 388cdb6ddd89faa28a88117b8e9cc82647cb5c02 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 3 May 2022 15:18:38 -0700 Subject: [PATCH 32/77] Fix bug where form was posting to the wrong path Signed-off-by: Margo Crawford --- internal/oidc/login/get_login_handler.go | 2 ++ internal/oidc/login/get_login_handler_test.go | 4 ++-- internal/oidc/login/login_form.gohtml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go index a8f90216..a34f487c 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/oidc/login/get_login_handler.go @@ -28,6 +28,7 @@ type PageData struct { HasAlertError bool AlertMessage string Title string + PostPath string } func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFunc { @@ -44,6 +45,7 @@ func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFun HasAlertError: alertError != "", AlertMessage: message, Title: "Pinniped", + PostPath: r.URL.Path, // the path for POST is the same as for GET }) if err != nil { return err diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index 7e4d8c56..3235fcc5 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -98,7 +98,7 @@ func TestGetLogin(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { handler := NewGetHandler(tt.idps) - target := "/login?state=" + tt.encodedState + target := "/some/path/login?state=" + tt.encodedState if tt.errParam != "" { target += "&err=" + tt.errParam } @@ -126,7 +126,7 @@ func getHTMLResult(errorBanner string) string {

Pinniped

some-ldap-idp

%s -
+
diff --git a/internal/oidc/login/login_form.gohtml b/internal/oidc/login/login_form.gohtml index 5376b5e4..6dd4819d 100644 --- a/internal/oidc/login/login_form.gohtml +++ b/internal/oidc/login/login_form.gohtml @@ -15,7 +15,7 @@ SPDX-License-Identifier: Apache-2.0 {{.AlertMessage}}
{{end}} - +
From acc6c50e48e99dc3166bd4afe331980ab6608f88 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 3 May 2022 15:43:01 -0700 Subject: [PATCH 33/77] More unit tests for LDAP DNs which contain special chars Adding explicit coverage for PerformRefresh(). --- internal/upstreamldap/upstreamldap_test.go | 398 +++++++++++---------- 1 file changed, 216 insertions(+), 182 deletions(-) diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index c8bdc395..b4ee6bdf 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -51,6 +51,8 @@ const ( testUserSearchResultUIDAttributeValue = "some-upstream-uid-value" testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1" testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2" + testUserDNWithSpecialChars = `user DN with * \ special characters ()` + testUserDNWithSpecialCharsEscaped = `user DN with \2a \5c special characters \28\29` expectedGroupSearchPageSize = uint32(250) ) @@ -529,7 +531,7 @@ func TestEndUserAuthentication(t *testing.T) { Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: `result DN with * \ special characters ()`, + DN: testUserDNWithSpecialChars, Attributes: []*ldap.EntryAttribute{ ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), @@ -538,16 +540,16 @@ func TestEndUserAuthentication(t *testing.T) { }, }, nil).Times(1) conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - escapedDN := `result DN with \2a \5c special characters \28\29` + escapedDN := testUserDNWithSpecialCharsEscaped r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", escapedDN, escapedDN) }), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1) conn.EXPECT().Close().Times(1) }, bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1) + conn.EXPECT().Bind(testUserDNWithSpecialChars, testUpstreamPassword).Times(1) }, wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) { - r.DN = `result DN with * \ special characters ()` + r.DN = testUserDNWithSpecialChars }), }, { @@ -563,7 +565,7 @@ func TestEndUserAuthentication(t *testing.T) { Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: `result DN with * \ special characters ()`, + DN: testUserDNWithSpecialChars, Attributes: []*ldap.EntryAttribute{ ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), @@ -572,15 +574,15 @@ func TestEndUserAuthentication(t *testing.T) { }, }, nil).Times(1) conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - r.Filter = fmt.Sprintf("(member=%s)", `result DN with \2a \5c special characters \28\29`) + r.Filter = fmt.Sprintf("(member=%s)", testUserDNWithSpecialCharsEscaped) }), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1) conn.EXPECT().Close().Times(1) }, bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1) + conn.EXPECT().Bind(testUserDNWithSpecialChars, testUpstreamPassword).Times(1) }, wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) { - r.DN = `result DN with * \ special characters ()` + r.DN = testUserDNWithSpecialChars }), }, { @@ -1219,28 +1221,41 @@ func TestEndUserAuthentication(t *testing.T) { func TestUpstreamRefresh(t *testing.T) { pwdLastSetAttribute := "pwdLastSet" - expectedUserSearch := &ldap.SearchRequest{ - BaseDN: testUserSearchResultDNValue, - Scope: ldap.ScopeBaseObject, - DerefAliases: ldap.NeverDerefAliases, - SizeLimit: 2, - TimeLimit: 90, - TypesOnly: false, - Filter: "(objectClass=*)", - Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute}, - Controls: nil, // don't need paging because we set the SizeLimit so small + + expectedUserSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { + request := &ldap.SearchRequest{ + BaseDN: testUserSearchResultDNValue, + Scope: ldap.ScopeBaseObject, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 2, + TimeLimit: 90, + TypesOnly: false, + Filter: "(objectClass=*)", + Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute}, + Controls: nil, // don't need paging because we set the SizeLimit so small + } + if editFunc != nil { + editFunc(request) + } + return request } - expectedGroupSearch := &ldap.SearchRequest{ - BaseDN: testGroupSearchBase, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - SizeLimit: 0, // unlimited size because we will search with paging - TimeLimit: 90, - TypesOnly: false, - Filter: testGroupSearchFilterInterpolated, - Attributes: []string{testGroupSearchGroupNameAttribute}, - Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us + expectedGroupSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { + request := &ldap.SearchRequest{ + BaseDN: testGroupSearchBase, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 0, // unlimited size because we will search with paging + TimeLimit: 90, + TypesOnly: false, + Filter: testGroupSearchFilterInterpolated, + Attributes: []string{testGroupSearchGroupNameAttribute}, + Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us + } + if editFunc != nil { + editFunc(request) + } + return request } happyPathUserSearchResult := &ldap.SearchResult{ @@ -1266,116 +1281,170 @@ func TestUpstreamRefresh(t *testing.T) { }, } - providerConfig := &ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ - Base: testUserSearchBase, - UIDAttribute: testUserSearchUIDAttribute, - UsernameAttribute: testUserSearchUsernameAttribute, - }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), + happyPathGroupSearchResult := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: testGroupSearchResultDNValue2, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}), + }, + }, }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + } + + providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { + config := &ProviderConfig{ + Name: "some-provider-name", + Host: testHost, + CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test + ConnectionProtocol: TLS, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: UserSearchConfig{ + Base: testUserSearchBase, + UIDAttribute: testUserSearchUIDAttribute, + UsernameAttribute: testUserSearchUsernameAttribute, + }, + GroupSearch: GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupSearchGroupNameAttribute, + }, + RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ + pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), + }, + } + if editFunc != nil { + editFunc(config) + } + return config } tests := []struct { name string providerConfig *ProviderConfig setupMocks func(conn *mockldapconn.MockConn) + refreshUserDN string dialError error wantErr string wantGroups []string }{ { - name: "happy path where searching the dn returns a single entry", - providerConfig: providerConfig, + name: "happy path without group search where searching the dn returns a single entry", + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch = GroupSearchConfig{} + }), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) conn.EXPECT().Close().Times(1) }, wantGroups: []string{}, }, { - name: "happy path where group search returns groups", - providerConfig: &ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ - Base: testUserSearchBase, - UIDAttribute: testUserSearchUIDAttribute, - UsernameAttribute: testUserSearchUsernameAttribute, - }, - GroupSearch: GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupSearchGroupNameAttribute, - }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), - }, - }, + name: "happy path where group search returns groups", + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: testGroupSearchResultDNValue2, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1) conn.EXPECT().Close().Times(1) }, wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, }, { - name: "happy path where group search returns no groups", - providerConfig: &ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ - Base: testUserSearchBase, - UIDAttribute: testUserSearchUIDAttribute, - UsernameAttribute: testUserSearchUsernameAttribute, - }, - GroupSearch: GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupSearchGroupNameAttribute, - }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), - }, - }, + name: "happy path when the user DN has special LDAP search filter characters then they must be properly escaped in the custom group search filter", + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.BaseDN = testUserDNWithSpecialChars + })). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserDNWithSpecialChars, + Attributes: []*ldap.EntryAttribute{ + { + Name: testUserSearchUsernameAttribute, + Values: []string{testUserSearchResultUsernameAttributeValue}, + }, + { + Name: testUserSearchUIDAttribute, + ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)}, + }, + { + Name: pwdLastSetAttribute, + Values: []string{"132801740800000000"}, + ByteValues: [][]byte{[]byte("132801740800000000")}, + }, + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", testUserDNWithSpecialCharsEscaped, testUserDNWithSpecialCharsEscaped) + }), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + refreshUserDN: testUserDNWithSpecialChars, + wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, + }, + { + name: "when the user DN has special LDAP search filter characters then they must be properly escaped in the default group search filter", + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.Filter = "" + }), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.BaseDN = testUserDNWithSpecialChars + })). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserDNWithSpecialChars, + Attributes: []*ldap.EntryAttribute{ + { + Name: testUserSearchUsernameAttribute, + Values: []string{testUserSearchResultUsernameAttributeValue}, + }, + { + Name: testUserSearchUIDAttribute, + ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)}, + }, + { + Name: pwdLastSetAttribute, + Values: []string{"132801740800000000"}, + ByteValues: [][]byte{[]byte("132801740800000000")}, + }, + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Filter = fmt.Sprintf("(member=%s)", testUserDNWithSpecialCharsEscaped) + }), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + refreshUserDN: testUserDNWithSpecialChars, + wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, + }, + { + name: "happy path where group search returns no groups", + providerConfig: providerConfig(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{}, Referrals: []string{}, // note that we are not following referrals at this time Controls: []ldap.Control{}, @@ -1386,44 +1455,25 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "happy path where group search is configured but skipGroupRefresh is set", - providerConfig: &ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ - Base: testUserSearchBase, - UIDAttribute: testUserSearchUIDAttribute, - UsernameAttribute: testUserSearchUsernameAttribute, - }, - GroupSearch: GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupSearchGroupNameAttribute, - SkipGroupRefresh: true, - }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), - }, - }, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.SkipGroupRefresh = true + }), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) // note that group search is not expected + conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) // note that group search is not expected conn.EXPECT().Close().Times(1) }, wantGroups: nil, // do not update groups }, { name: "error where dial fails", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), dialError: errors.New("some dial error"), wantErr: "error dialing host \"ldap.example.com:8443\": some dial error", }, { name: "error binding", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) conn.EXPECT().Close().Times(1) @@ -1432,10 +1482,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result returns no entries", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{}, }, nil).Times(1) conn.EXPECT().Close().Times(1) @@ -1444,20 +1494,20 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "error searching", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(nil, errors.New("some search error")) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(nil, errors.New("some search error")) conn.EXPECT().Close().Times(1) }, wantErr: "error searching for user \"some-upstream-user-dn\": some search error", }, { name: "search result returns more than one entry", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1475,10 +1525,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has wrong uid", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1501,10 +1551,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has wrong username", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1523,10 +1573,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has no dn", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { Attributes: []*ldap.EntryAttribute{ @@ -1548,10 +1598,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has 0 values for username attribute", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1574,10 +1624,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has more than one value for username attribute", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1600,10 +1650,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has 0 values for uid attribute", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1626,10 +1676,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has 2 values for uid attribute", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1652,10 +1702,10 @@ func TestUpstreamRefresh(t *testing.T) { }, { name: "search result has a changed pwdLastSet value", - providerConfig: providerConfig, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { DN: testUserSearchResultDNValue, @@ -1681,32 +1731,12 @@ func TestUpstreamRefresh(t *testing.T) { wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login", }, { - name: "group search returns an error", - providerConfig: &ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ - Base: testUserSearchBase, - UIDAttribute: testUserSearchUIDAttribute, - UsernameAttribute: testUserSearchUsernameAttribute, - }, - GroupSearch: GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupSearchGroupNameAttribute, - }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ - pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), - }, - }, + name: "group search returns an error", + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(nil, errors.New("some search error")).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(nil, errors.New("some search error")).Times(1) conn.EXPECT().Close().Times(1) }, wantErr: "error searching for group memberships for user with DN \"some-upstream-user-dn\": some search error", @@ -1735,13 +1765,17 @@ func TestUpstreamRefresh(t *testing.T) { return conn, nil }) + if tt.refreshUserDN == "" { + tt.refreshUserDN = testUserSearchResultDNValue // default for all tests + } + initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000")) ldapProvider := New(*tt.providerConfig) subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" groups, err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{ Username: testUserSearchResultUsernameAttributeValue, Subject: subject, - DN: testUserSearchResultDNValue, + DN: tt.refreshUserDN, AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded}, }) if tt.wantErr != "" { From 2e031f727b2fc9390a6f4f65f4fe608d4983725b Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 3 May 2022 16:46:09 -0700 Subject: [PATCH 34/77] Use security headers for the form_post page in the POST /login endpoint Also use more specific test assertions where security headers are expected. And run the unit tests for the login package in parallel. --- internal/oidc/auth/auth_handler_test.go | 2 +- .../oidc/callback/callback_handler_test.go | 2 +- internal/oidc/login/get_login_handler_test.go | 5 ++- internal/oidc/login/login_handler.go | 18 +++++++++- internal/oidc/login/login_handler_test.go | 12 +++++-- .../oidc/login/post_login_handler_test.go | 30 ++++++++-------- internal/testutil/assertions.go | 34 ++++++++++++++++--- 7 files changed, 78 insertions(+), 25 deletions(-) diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index fc0cbc53..058cb70c 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -2566,7 +2566,7 @@ func TestAuthorizationEndpoint(t *testing.T) { require.Equal(t, test.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType) - testutil.RequireSecurityHeaders(t, rsp) + testutil.RequireSecurityHeadersWithoutFormPostCSPs(t, rsp) if test.wantPasswordGrantCall != nil { test.wantPasswordGrantCall.args.Ctx = reqContext diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 6fc47773..e92974d9 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -1034,7 +1034,7 @@ func TestCallbackEndpoint(t *testing.T) { t.Logf("response: %#v", rsp) t.Logf("response body: %q", rsp.Body.String()) - testutil.RequireSecurityHeaders(t, rsp) + testutil.RequireSecurityHeadersWithFormPostCSPs(t, rsp) if test.wantAuthcodeExchangeCall != nil { test.wantAuthcodeExchangeCall.args.Ctx = reqContext diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index 3235fcc5..484ee450 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -96,7 +96,10 @@ func TestGetLogin(t *testing.T) { for _, test := range tests { tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + handler := NewGetHandler(tt.idps) target := "/some/path/login?state=" + tt.encodedState if tt.errParam != "" { @@ -107,7 +110,7 @@ func TestGetLogin(t *testing.T) { err := handler(rsp, req, tt.encodedState, tt.decodedState) require.NoError(t, err) - require.Equal(t, test.wantStatus, rsp.Code) + require.Equal(t, tt.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) body := rsp.Body.String() require.Equal(t, tt.wantBody, body) diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go index 751dc9c4..ce1b3810 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/oidc/login/login_handler.go @@ -11,6 +11,7 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" ) @@ -78,7 +79,22 @@ func NewHandler( return handler(w, r, encodedState, decodedState) }) - return securityheader.Wrap(loginHandler) + return wrapSecurityHeaders(loginHandler) +} + +func wrapSecurityHeaders(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var wrapped http.Handler + switch r.Method { + case http.MethodPost: + // POST requests can result in the form_post html page, so allow it with CSP headers. + wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy()) + default: + wrapped = securityheader.Wrap(handler) + } + + wrapped.ServeHTTP(w, r) + }) } func RedirectToLoginPage( diff --git a/internal/oidc/login/login_handler_test.go b/internal/oidc/login/login_handler_test.go index 347f0760..79a0ee65 100644 --- a/internal/oidc/login/login_handler_test.go +++ b/internal/oidc/login/login_handler_test.go @@ -370,9 +370,11 @@ func TestLoginEndpoint(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(tt.method, tt.path, nil) - if test.csrfCookie != "" { - req.Header.Set("Cookie", test.csrfCookie) + if tt.csrfCookie != "" { + req.Header.Set("Cookie", tt.csrfCookie) } rsp := httptest.NewRecorder() @@ -414,7 +416,11 @@ func TestLoginEndpoint(t *testing.T) { subject.ServeHTTP(rsp, req) - testutil.RequireSecurityHeaders(t, rsp) + if tt.method == http.MethodPost { + testutil.RequireSecurityHeadersWithFormPostCSPs(t, rsp) + } else { + testutil.RequireSecurityHeadersWithoutFormPostCSPs(t, rsp) + } require.Equal(t, tt.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) diff --git a/internal/oidc/login/post_login_handler_test.go b/internal/oidc/login/post_login_handler_test.go index 1e4fa437..f74d67d8 100644 --- a/internal/oidc/login/post_login_handler_test.go +++ b/internal/oidc/login/post_login_handler_test.go @@ -617,6 +617,8 @@ func TestPostLoginEndpoint(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { + t.Parallel() + kubeClient := fake.NewSimpleClientset() secretsClient := kubeClient.CoreV1().Secrets("some-namespace") @@ -650,7 +652,7 @@ func TestPostLoginEndpoint(t *testing.T) { require.Equal(t, tt.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) - require.Equal(t, test.wantBodyString, rsp.Body.String()) + require.Equal(t, tt.wantBodyString, rsp.Body.String()) actualLocation := rsp.Header().Get("Location") @@ -660,30 +662,30 @@ func TestPostLoginEndpoint(t *testing.T) { oidctestutil.RequireAuthCodeRegexpMatch( t, actualLocation, - test.wantRedirectLocationRegexp, + tt.wantRedirectLocationRegexp, kubeClient, secretsClient, kubeOauthStore, - test.wantDownstreamGrantedScopes, - test.wantDownstreamIDTokenSubject, - test.wantDownstreamIDTokenUsername, - test.wantDownstreamIDTokenGroups, - test.wantDownstreamRequestedScopes, - test.wantDownstreamPKCEChallenge, - test.wantDownstreamPKCEChallengeMethod, - test.wantDownstreamNonce, + tt.wantDownstreamGrantedScopes, + tt.wantDownstreamIDTokenSubject, + tt.wantDownstreamIDTokenUsername, + tt.wantDownstreamIDTokenGroups, + tt.wantDownstreamRequestedScopes, + tt.wantDownstreamPKCEChallenge, + tt.wantDownstreamPKCEChallengeMethod, + tt.wantDownstreamNonce, downstreamClientID, - test.wantDownstreamRedirectURI, - test.wantDownstreamCustomSessionData, + tt.wantDownstreamRedirectURI, + tt.wantDownstreamCustomSessionData, ) case tt.wantRedirectToLoginPageError != "": expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath + "?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState require.Equal(t, expectedLocation, actualLocation) - require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords) + require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) case tt.wantRedirectLocationString != "": require.Equal(t, tt.wantRedirectLocationString, actualLocation) - require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords) + require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) default: require.Failf(t, "test should have expected a redirect", "actual location was %q", actualLocation) diff --git a/internal/testutil/assertions.go b/internal/testutil/assertions.go index 9286bff1..b592e07e 100644 --- a/internal/testutil/assertions.go +++ b/internal/testutil/assertions.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testutil @@ -54,9 +54,35 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets) } -func RequireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) { - // 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'") +func RequireSecurityHeadersWithFormPostCSPs(t *testing.T, response *httptest.ResponseRecorder) { + // Loosely confirm that the unique CSPs needed for the form_post page were used. + cspHeader := response.Header().Get("Content-Security-Policy") + require.Contains(t, cspHeader, "script-src '") // loose assertion + require.Contains(t, cspHeader, "style-src '") // loose assertion + require.Contains(t, cspHeader, "img-src data:") + require.Contains(t, cspHeader, "connect-src *") + + // Also require all the usual security headers. + requireSecurityHeaders(t, response) +} + +func RequireSecurityHeadersWithoutFormPostCSPs(t *testing.T, response *httptest.ResponseRecorder) { + // Confirm that the unique CSPs needed for the form_post page were NOT used. + cspHeader := response.Header().Get("Content-Security-Policy") + require.NotContains(t, cspHeader, "script-src") + require.NotContains(t, cspHeader, "style-src") + require.NotContains(t, cspHeader, "img-src data:") + require.NotContains(t, cspHeader, "connect-src *") + + // Also require all the usual security headers. + requireSecurityHeaders(t, response) +} + +func requireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) { + // Loosely confirm that the generic CSPs were used. + cspHeader := response.Header().Get("Content-Security-Policy") + require.Contains(t, cspHeader, "default-src 'none'") + require.Contains(t, cspHeader, "frame-ancestors 'none'") require.Equal(t, "DENY", response.Header().Get("X-Frame-Options")) require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection")) From eb891d77a5a2d03bbf372ebb76026d3bdb5cbb68 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 4 May 2022 12:42:55 -0700 Subject: [PATCH 35/77] Tiny fix: pinninpeds->pinnipeds Signed-off-by: Margo Crawford --- test/deploy/tools/ldap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index 831dd6e2..94638ecf 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -1,4 +1,4 @@ -#! Copyright 2021 the Pinniped contributors. All Rights Reserved. +#! Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") @@ -114,7 +114,7 @@ ldap.ldif: | dn: cn=mammals,ou=groups,dc=pinniped,dc=dev cn: mammals objectClass: groupOfNames - member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev + member: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev member: cn=olive,ou=users,dc=pinniped,dc=dev #@ end From 1a59b6a686f201a8d2be674ea65418c1badb8387 Mon Sep 17 00:00:00 2001 From: anjalitelang <49958114+anjaltelang@users.noreply.github.com> Date: Wed, 4 May 2022 16:06:33 -0400 Subject: [PATCH 36/77] Update ROADMAP.md Changes made to reflect status as of May 4th, 2022 --- ROADMAP.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 49420bb6..c80700de 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -36,9 +36,8 @@ The following table includes the current roadmap for Pinniped. If you have any q Last Updated: March 2022 |Theme|Description|Timeline| |--|--|--| -|Improving Security Posture|Support FIPS compliant Boring crypto libraries |March/April 2022| -|Improving Security Posture|Support Audit logging of security events related to Authentication |April/May 2022| -|Improving Usability|Support for integrating with UI/Dashboards |June/July 2022| +|Improving Security Posture|Support Audit logging of security events related to Authentication |May/June 2022| +|Improving Usability|Support for integrating with UI/Dashboards |May/June 2022| |Improving Security Posture|TLS hardening contd|June/July 2022| |Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Exploring/Ongoing| |Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing| From 079908fb50be7f074ff2a80ab2422fe4bbd395a3 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 4 May 2022 13:28:54 -0700 Subject: [PATCH 37/77] Update to reflect further conversations we've had Signed-off-by: Margo Crawford --- proposals/1113_ldap-ad-web-ui/README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 70d701ed..99571d58 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -102,7 +102,10 @@ logo and the IDP name. 2. Using the idp name/type from the state param, look up the IDP, bind to it, verify the username/password and get the users downstream username and groups. 3. If the login succeeds, mint an authcode and store the session as a secret the same way as we do on the - callback endpoint today. Redirect to the client’s redirect URI with the new authcode. + callback endpoint today, and return the new authcode. If `response_mode=form_post` was requested, return a 200 + with Pinniped's form post html page, to be displayed on the login page. If it is `query`, return a redirect + with the authcode as a query param. Default behavior when `response_mode` is unspecified should be handled + by other parts of the code, but it should default to `query` on the supervisor. 4. If the login fails, respond with an error so the login page can render an error message. Allow the user to retry login the same way we do with the CLI today (we leave brute force protection to the IDP) @@ -119,6 +122,20 @@ However if they do choose to register a new client they may need to update the f - The name of the idp custom resource is currently not published to users logging in with Pinniped. We plan on exposing this to indicate to users which idp they are logging in to. Admins may need to update this to something more user-friendly. + Note: While branding is an important part of the user experience, and we may consider adding + the option to customize the page or add new fields (such as an IDP "display name" field), we + are choosing to defer this work until later. We want to get the MVP work done and into users' + hands and hope to hear more from the community once the MVP is completed. + For the MVP, we should not add new config but should remind admins that the IDP field field + is now displayed. + +To enable users to upgrade smoothly, the behavior of the Pinniped CLI when it encounters multiple possible flow options will change. +Previously, the team had decided that the CLI should fail when there were multiple options (e.g. when it's could +use either the `browser_authcode` flow or the `cli_password` flow). However, that behavior would break existing +kubeconfigs once the `browser_authcode` flow was introduced to the IDP discovery doc. +Instead we are opting to prioritize based on the order listed in the IDP discovery doc. +Users will still have the option to override this priority with the `--upstream-identity-provider-flow` flag, +but that flag will not be required. #### Tests @@ -139,6 +156,8 @@ Once dynamic clients are implemented: #### New Dependencies This should be kept to a very simple HTML page with minimal, clean CSS styling. Javascript should be avoided. +The styling should match the [form post html page](https://github.com/vmware-tanzu/pinniped/tree/main/internal/oidc/provider/formposthtml) +as much as possible, we should reuse some of the existing css and add to it to keep the style consistent. #### Observability Considerations * Logging login attempts at higher log levels. From 329d41aac7cc8b39671313b0f8c657ea99802baf Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 5 May 2022 08:49:58 -0700 Subject: [PATCH 38/77] Add the full end to end test for ldap web ui Signed-off-by: Margo Crawford --- test/integration/e2e_test.go | 37 ++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 13a70757..e4a73b66 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -974,6 +974,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo page := browsertest.Open(t) expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs setupClusterForEndToEndLDAPTest(t, expectedUsername, env) @@ -992,6 +993,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }) // Run "kubectl get namespaces" which should trigger a browser login via the plugin. + start := time.Now() kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) @@ -1078,8 +1080,39 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo regex := regexp.MustCompile(`\A` + downstream.Spec.Issuer + `/login.+`) browsertest.WaitForURL(t, page, regex) - browsertest.WaitForVisibleElements(t, page, "input#username", "input#password", "button#submit") - // TODO actually log in :P + usernameSelector := "input#username" + passwordSelector := "input[type='password']" + loginButtonSelector := "button#submit" + browsertest.WaitForVisibleElements(t, page, usernameSelector, passwordSelector, loginButtonSelector) + + // Fill in the username and password and click "submit". + t.Logf("logging into ldap") + require.NoError(t, page.First(usernameSelector).Fill(expectedUsername)) + require.NoError(t, page.First(passwordSelector).Fill(env.SupervisorUpstreamLDAP.TestUserPassword)) + require.NoError(t, page.First(loginButtonSelector).Click()) + + formpostExpectSuccessState(t, page) + + // Expect the CLI to output a list of namespaces. + t.Logf("waiting for kubectl to output namespace list") + var kubectlOutput string + select { + case <-time.After(1 * time.Minute): + require.Fail(t, "timed out waiting for kubectl output") + case kubectlOutput = <-kubectlOutputChan: + } + requireKubectlGetNamespaceOutput(t, env, kubectlOutput) + + t.Logf("first kubectl command took %s", time.Since(start).String()) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) }) } From 6ca7c932aec6b923f40db175fe901c2bda14cb77 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 4 May 2022 12:12:14 -0700 Subject: [PATCH 39/77] Add unit test for rendering form_post response from POST /login --- internal/oidc/login/login_handler.go | 8 +-- .../oidc/login/post_login_handler_test.go | 57 ++++++++++++++++++- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go index ce1b3810..eb9d8251 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/oidc/login/login_handler.go @@ -84,15 +84,11 @@ func NewHandler( func wrapSecurityHeaders(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var wrapped http.Handler - switch r.Method { - case http.MethodPost: + wrapped := securityheader.Wrap(handler) + if r.Method == http.MethodPost { // POST requests can result in the form_post html page, so allow it with CSP headers. wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy()) - default: - wrapped = securityheader.Wrap(handler) } - wrapped.ServeHTTP(w, r) }) } diff --git a/internal/oidc/login/post_login_handler_test.go b/internal/oidc/login/post_login_handler_test.go index f74d67d8..267c5e08 100644 --- a/internal/oidc/login/post_login_handler_test.go +++ b/internal/oidc/login/post_login_handler_test.go @@ -249,6 +249,7 @@ func TestPostLoginEndpoint(t *testing.T) { // upstream LDAP or AD provider. wantRedirectLocationRegexp string // for loose matching wantRedirectLocationString string // for exact matching instead + wantBodyFormResponseRegexp string // for form_post html page matching instead wantDownstreamRedirectURI string wantDownstreamGrantedScopes []string wantDownstreamIDTokenSubject string @@ -311,6 +312,30 @@ func TestPostLoginEndpoint(t *testing.T) { wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, }, + { + name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), + decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { + query := copyOfHappyDownstreamRequestParamsQuery() + query["response_mode"] = []string{"form_post"} + data.AuthParams = query.Encode() + }), + formParams: happyUsernamePasswordFormParams, + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBodyFormResponseRegexp: `(?s).*To finish logging in, paste this authorization code` + + `.*.*(.+).*`, // "(?s)" means match "." across newlines + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, + }, { name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), @@ -652,12 +677,13 @@ func TestPostLoginEndpoint(t *testing.T) { require.Equal(t, tt.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) - require.Equal(t, tt.wantBodyString, rsp.Body.String()) actualLocation := rsp.Header().Get("Location") switch { case tt.wantRedirectLocationRegexp != "": + // Expecting a success redirect to the client. + require.Equal(t, tt.wantBodyString, rsp.Body.String()) require.Len(t, rsp.Header().Values("Location"), 1) oidctestutil.RequireAuthCodeRegexpMatch( t, @@ -679,15 +705,42 @@ func TestPostLoginEndpoint(t *testing.T) { tt.wantDownstreamCustomSessionData, ) case tt.wantRedirectToLoginPageError != "": + // Expecting an error redirect to the login UI page. + require.Equal(t, tt.wantBodyString, rsp.Body.String()) expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath + "?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState require.Equal(t, expectedLocation, actualLocation) require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) case tt.wantRedirectLocationString != "": + // Expecting an error redirect to the client. + require.Equal(t, tt.wantBodyString, rsp.Body.String()) require.Equal(t, tt.wantRedirectLocationString, actualLocation) require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) + case tt.wantBodyFormResponseRegexp != "": + // Expecting the body of the response to be a html page with a form (for "response_mode=form_post"). + _, hasLocationHeader := rsp.Header()["Location"] + require.False(t, hasLocationHeader) + oidctestutil.RequireAuthCodeRegexpMatch( + t, + rsp.Body.String(), + tt.wantBodyFormResponseRegexp, + kubeClient, + secretsClient, + kubeOauthStore, + tt.wantDownstreamGrantedScopes, + tt.wantDownstreamIDTokenSubject, + tt.wantDownstreamIDTokenUsername, + tt.wantDownstreamIDTokenGroups, + tt.wantDownstreamRequestedScopes, + tt.wantDownstreamPKCEChallenge, + tt.wantDownstreamPKCEChallengeMethod, + tt.wantDownstreamNonce, + downstreamClientID, + tt.wantDownstreamRedirectURI, + tt.wantDownstreamCustomSessionData, + ) default: - require.Failf(t, "test should have expected a redirect", + require.Failf(t, "test should have expected a redirect or form body", "actual location was %q", actualLocation) } }) From cffa353ffb76983dc172a9d211158ddbaf0155df Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 5 May 2022 13:12:06 -0700 Subject: [PATCH 40/77] Login page styling/structure for users, screen readers, passwd managers Also: - Add CSS to login page - Refactor login page HTML and CSS into a new package - New custom CSP headers for the login page, because the requirements are different from the form_post page --- internal/oidc/auth/auth_handler_test.go | 2 +- .../oidc/callback/callback_handler_test.go | 2 +- internal/oidc/login/get_login_handler.go | 60 ++++------ internal/oidc/login/get_login_handler_test.go | 106 ++++++------------ internal/oidc/login/login_form.gohtml | 39 ------- internal/oidc/login/login_handler.go | 3 +- internal/oidc/login/login_handler_test.go | 4 +- internal/oidc/login/loginhtml/login_form.css | 94 ++++++++++++++++ .../oidc/login/loginhtml/login_form.gohtml | 40 +++++++ internal/oidc/login/loginhtml/loginhtml.go | 65 +++++++++++ .../oidc/login/loginhtml/loginhtml_test.go | 68 +++++++++++ internal/oidc/provider/csp/csp.go | 15 +++ internal/oidc/provider/csp/csp_test.go | 15 +++ .../provider/formposthtml/formposthtml.go | 15 +-- .../formposthtml/formposthtml_test.go | 4 - internal/oidc/provider/manager/manager.go | 2 +- internal/testutil/assertions.go | 20 +++- internal/testutil/loginhtml.go | 68 +++++++++++ 18 files changed, 449 insertions(+), 173 deletions(-) delete mode 100644 internal/oidc/login/login_form.gohtml create mode 100644 internal/oidc/login/loginhtml/login_form.css create mode 100644 internal/oidc/login/loginhtml/login_form.gohtml create mode 100644 internal/oidc/login/loginhtml/loginhtml.go create mode 100644 internal/oidc/login/loginhtml/loginhtml_test.go create mode 100644 internal/oidc/provider/csp/csp.go create mode 100644 internal/oidc/provider/csp/csp_test.go create mode 100644 internal/testutil/loginhtml.go diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 058cb70c..e2b3f000 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -2566,7 +2566,7 @@ func TestAuthorizationEndpoint(t *testing.T) { require.Equal(t, test.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType) - testutil.RequireSecurityHeadersWithoutFormPostCSPs(t, rsp) + testutil.RequireSecurityHeadersWithoutCustomCSPs(t, rsp) if test.wantPasswordGrantCall != nil { test.wantPasswordGrantCall.args.Ctx = reqContext diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index e92974d9..d8f08822 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -1034,7 +1034,7 @@ func TestCallbackEndpoint(t *testing.T) { t.Logf("response: %#v", rsp) t.Logf("response body: %q", rsp.Body.String()) - testutil.RequireSecurityHeadersWithFormPostCSPs(t, rsp) + testutil.RequireSecurityHeadersWithFormPostPageCSPs(t, rsp) if test.wantAuthcodeExchangeCall != nil { test.wantAuthcodeExchangeCall.args.Ctx = reqContext diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go index a34f487c..3e33c937 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/oidc/login/get_login_handler.go @@ -4,53 +4,39 @@ package login import ( - _ "embed" - "html/template" "net/http" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/login/loginhtml" ) -const defaultErrorMessage = "An internal error occurred. Please contact your administrator for help." - -var ( - //go:embed login_form.gohtml - rawHTMLTemplate string - - errorMappings = map[string]string{ - "login_error": "Incorrect username or password.", - } +const ( + internalErrorMessage = "An internal error occurred. Please contact your administrator for help." + incorrectUsernameOrPasswordErrorMessage = "Incorrect username or password." ) -type PageData struct { - State string - IDPName string - HasAlertError bool - AlertMessage string - Title string - PostPath string -} - -func NewGetHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) HandlerFunc { - var parsedHTMLTemplate = template.Must(template.New("login_post.gohtml").Parse(rawHTMLTemplate)) +func NewGetHandler() HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { - alertError := r.URL.Query().Get("err") - message := errorMappings[alertError] - if message == "" { - message = defaultErrorMessage - } - err := parsedHTMLTemplate.Execute(w, &PageData{ + alertMessage, hasAlert := getAlert(r) + + pageInputs := &loginhtml.PageData{ + PostPath: r.URL.Path, // the path for POST is the same as for GET State: encodedState, IDPName: decodedState.UpstreamName, - HasAlertError: alertError != "", - AlertMessage: message, - Title: "Pinniped", - PostPath: r.URL.Path, // the path for POST is the same as for GET - }) - if err != nil { - return err + HasAlertError: hasAlert, + AlertMessage: alertMessage, } - - return nil + return loginhtml.Template().Execute(w, pageInputs) } } + +func getAlert(r *http.Request) (string, bool) { + errorParamValue := r.URL.Query().Get(errParamName) + + message := internalErrorMessage + if errorParamValue == string(ShowBadUserPassErr) { + message = incorrectUsernameOrPasswordErrorMessage + } + + return message, errorParamValue != "" +} diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index 484ee450..472148d5 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -4,21 +4,23 @@ package login import ( - "fmt" "net/http" "net/http/httptest" "testing" - "go.pinniped.dev/internal/testutil" - "github.com/stretchr/testify/require" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/login/loginhtml" + "go.pinniped.dev/internal/testutil" ) func TestGetLogin(t *testing.T) { const ( - happyLdapIDPName = "some-ldap-idp" + testPath = "/some/path/login" + testUpstreamName = "some-ldap-idp" + testUpstreamType = "ldap" + testEncodedState = "fake-encoded-state-value" ) tests := []struct { @@ -34,63 +36,57 @@ func TestGetLogin(t *testing.T) { { name: "Happy path ldap", decodedState: &oidc.UpstreamStateParamData{ - UpstreamName: happyLdapIDPName, - UpstreamType: "ldap", + UpstreamName: testUpstreamName, + UpstreamType: testUpstreamType, }, - encodedState: "foo", // the encoded and decoded state don't match, but that verification is handled one level up. + encodedState: testEncodedState, // the encoded and decoded state don't match, but that verification is handled one level up. wantStatus: http.StatusOK, wantContentType: htmlContentType, - wantBody: getHTMLResult(""), + wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState, ""), // no alert message }, { name: "displays error banner when err=login_error param is sent", decodedState: &oidc.UpstreamStateParamData{ - UpstreamName: happyLdapIDPName, - UpstreamType: "ldap", + UpstreamName: testUpstreamName, + UpstreamType: testUpstreamType, }, - encodedState: "foo", + encodedState: testEncodedState, errParam: "login_error", wantStatus: http.StatusOK, wantContentType: htmlContentType, - wantBody: getHTMLResult(` -
- Incorrect username or password. -
-`), + wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState, + "Incorrect username or password.", + ), }, { name: "displays error banner when err=internal_error param is sent", decodedState: &oidc.UpstreamStateParamData{ - UpstreamName: happyLdapIDPName, - UpstreamType: "ldap", + UpstreamName: testUpstreamName, + UpstreamType: testUpstreamType, }, - encodedState: "foo", + encodedState: testEncodedState, errParam: "internal_error", wantStatus: http.StatusOK, wantContentType: htmlContentType, - wantBody: getHTMLResult(` -
- An internal error occurred. Please contact your administrator for help. -
-`), + wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState, + "An internal error occurred. Please contact your administrator for help.", + ), }, - // If we get an error that we don't recognize, that's also an error, so we - // should probably just tell you to contact your administrator... { + // If we get an error that we don't recognize, that's also an error, so we + // should probably just tell you to contact your administrator... name: "displays generic error banner when unrecognized err param is sent", decodedState: &oidc.UpstreamStateParamData{ - UpstreamName: happyLdapIDPName, - UpstreamType: "ldap", + UpstreamName: testUpstreamName, + UpstreamType: testUpstreamType, }, - encodedState: "foo", + encodedState: testEncodedState, errParam: "some_other_error", wantStatus: http.StatusOK, wantContentType: htmlContentType, - wantBody: getHTMLResult(` -
- An internal error occurred. Please contact your administrator for help. -
-`), + wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState, + "An internal error occurred. Please contact your administrator for help.", + ), }, } @@ -100,8 +96,8 @@ func TestGetLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - handler := NewGetHandler(tt.idps) - target := "/some/path/login?state=" + tt.encodedState + handler := NewGetHandler() + target := testPath + "?state=" + tt.encodedState if tt.errParam != "" { target += "&err=" + tt.errParam } @@ -113,44 +109,8 @@ func TestGetLogin(t *testing.T) { require.Equal(t, tt.wantStatus, rsp.Code) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType) body := rsp.Body.String() + // t.Log("actual body:", body) // useful when updating expected values require.Equal(t, tt.wantBody, body) }) } } - -func getHTMLResult(errorBanner string) string { - happyGetResult := ` - - - Pinniped - - - -

Pinniped

-

some-ldap-idp

-%s - - -
- - -
- -
- - -
- -
- -
- - - - - - - -` - return fmt.Sprintf(happyGetResult, errorBanner) -} diff --git a/internal/oidc/login/login_form.gohtml b/internal/oidc/login/login_form.gohtml deleted file mode 100644 index 6dd4819d..00000000 --- a/internal/oidc/login/login_form.gohtml +++ /dev/null @@ -1,39 +0,0 @@ - - - - {{.Title}} - - - -

Pinniped

-

{{ .IDPName }}

-{{if .HasAlertError}} -
- {{.AlertMessage}} -
-{{end}} -
- -
- - -
- -
- - -
- -
- -
- - - -
- - - diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go index eb9d8251..06444bc1 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/oidc/login/login_handler.go @@ -11,6 +11,7 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/login/loginhtml" "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" ) @@ -84,7 +85,7 @@ func NewHandler( func wrapSecurityHeaders(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wrapped := securityheader.Wrap(handler) + wrapped := securityheader.WrapWithCustomCSP(handler, loginhtml.ContentSecurityPolicy()) if r.Method == http.MethodPost { // POST requests can result in the form_post html page, so allow it with CSP headers. wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy()) diff --git a/internal/oidc/login/login_handler_test.go b/internal/oidc/login/login_handler_test.go index 79a0ee65..11380950 100644 --- a/internal/oidc/login/login_handler_test.go +++ b/internal/oidc/login/login_handler_test.go @@ -417,9 +417,9 @@ func TestLoginEndpoint(t *testing.T) { subject.ServeHTTP(rsp, req) if tt.method == http.MethodPost { - testutil.RequireSecurityHeadersWithFormPostCSPs(t, rsp) + testutil.RequireSecurityHeadersWithFormPostPageCSPs(t, rsp) } else { - testutil.RequireSecurityHeadersWithoutFormPostCSPs(t, rsp) + testutil.RequireSecurityHeadersWithLoginPageCSPs(t, rsp) } require.Equal(t, tt.wantStatus, rsp.Code) diff --git a/internal/oidc/login/loginhtml/login_form.css b/internal/oidc/login/loginhtml/login_form.css new file mode 100644 index 00000000..5eba47e0 --- /dev/null +++ b/internal/oidc/login/loginhtml/login_form.css @@ -0,0 +1,94 @@ +/* Copyright 2022 the Pinniped contributors. All Rights Reserved. */ +/* SPDX-License-Identifier: Apache-2.0 */ + +html { + height: 100%; +} + +body { + font-family: "Metropolis-Light", Helvetica, sans-serif; + display: flex; + flex-flow: column wrap; + justify-content: flex-start; + align-items: center; + /* subtle gradient make the login box stand out */ + background: linear-gradient(to top, #f8f8f8, white); + min-height: 100%; +} + +h1 { + font-size: 20px; + margin: 0; +} + +.box { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + border-radius: 4px; + border-color: #ddd; + border-width: 1px; + border-style: solid; + width: 400px; + padding:30px 30px 0; + margin: 60px 20px 0; + background: white; + font-size: 14px; +} + +input { + color: inherit; + font: inherit; + border: 0; + margin: 0; + outline: 0; + padding: 0; +} + +.form-field { + display: flex; + margin-bottom: 30px; +} + +.form-field input[type="password"], .form-field input[type="text"], .form-field input[type="submit"] { + width: 100%; + padding: 1em; +} + +.form-field input[type="password"], .form-field input[type="text"] { + border-radius: 3px; + border-width: 1px; + border-style: solid; + border-color: #a6a6a6; +} + +.form-field input[type="submit"] { + background-color: #218fcf; /* this is a color from the Pinniped logo :) */ + color: #eee; + font-weight: bold; + cursor: pointer; + transition: all .3s; +} + +.form-field input[type="submit"]:focus, .form-field input[type="submit"]:hover { + background-color: #1abfd3; /* this is a color from the Pinniped logo :) */ +} + +.form-field input[type="submit"]:active { + transform: scale(.99); +} + +.hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.alert { + color: crimson; +} diff --git a/internal/oidc/login/loginhtml/login_form.gohtml b/internal/oidc/login/loginhtml/login_form.gohtml new file mode 100644 index 00000000..a92e406f --- /dev/null +++ b/internal/oidc/login/loginhtml/login_form.gohtml @@ -0,0 +1,40 @@ + + + + Pinniped + + + + + +
+
+

Log in to {{.IDPName}}

+
+ {{if .HasAlertError}} +
+ {{.AlertMessage}} +
+ {{end}} +
+ +
+ + +
+
+ + +
+
+ +
+
+
+ + diff --git a/internal/oidc/login/loginhtml/loginhtml.go b/internal/oidc/login/loginhtml/loginhtml.go new file mode 100644 index 00000000..1493979f --- /dev/null +++ b/internal/oidc/login/loginhtml/loginhtml.go @@ -0,0 +1,65 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package loginhtml defines HTML templates used by the Supervisor. +//nolint: gochecknoglobals // This package uses globals to ensure that all parsing and minifying happens at init. +package loginhtml + +import ( + _ "embed" // Needed to trigger //go:embed directives below. + "html/template" + "strings" + + "github.com/tdewolff/minify/v2/minify" + + "go.pinniped.dev/internal/oidc/provider/csp" +) + +var ( + //go:embed login_form.css + rawCSS string + minifiedCSS = mustMinify(minify.CSS(rawCSS)) + + //go:embed login_form.gohtml + rawHTMLTemplate string +) + +// Parse the Go templated HTML and inject functions providing the minified inline CSS and JS. +var parsedHTMLTemplate = template.Must(template.New("login_form.gohtml").Funcs(template.FuncMap{ + "minifiedCSS": func() template.CSS { return template.CSS(minifiedCSS) }, +}).Parse(rawHTMLTemplate)) + +// Generate the CSP header value once since it's effectively constant. +var cspValue = strings.Join([]string{ + `default-src 'none'`, + `style-src '` + csp.Hash(minifiedCSS) + `'`, + `frame-ancestors 'none'`, +}, "; ") + +func mustMinify(s string, err error) string { + if err != nil { + panic(err) + } + return s +} + +// 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. +func ContentSecurityPolicy() string { return cspValue } + +// Template returns the html/template.Template for rendering the login page. +func Template() *template.Template { return parsedHTMLTemplate } + +// CSS returns the minified CSS that will be embedded into the page template. +func CSS() string { return minifiedCSS } + +// PageData represents the inputs to the template. +type PageData struct { + State string + IDPName string + HasAlertError bool + AlertMessage string + MinifiedCSS template.CSS + PostPath string +} diff --git a/internal/oidc/login/loginhtml/loginhtml_test.go b/internal/oidc/login/loginhtml/loginhtml_test.go new file mode 100644 index 00000000..a2e91ed1 --- /dev/null +++ b/internal/oidc/login/loginhtml/loginhtml_test.go @@ -0,0 +1,68 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package loginhtml + +import ( + "bytes" + "fmt" + "testing" + + "go.pinniped.dev/internal/testutil" + + "github.com/stretchr/testify/require" +) + +var ( + testExpectedCSS = `html{height:100%}body{font-family:metropolis-light,Helvetica,sans-serif;display:flex;flex-flow:column wrap;justify-content:flex-start;align-items:center;background:linear-gradient(to top,#f8f8f8,white);min-height:100%}h1{font-size:20px;margin:0}.box{display:flex;flex-direction:column;flex-wrap:nowrap;border-radius:4px;border-color:#ddd;border-width:1px;border-style:solid;width:400px;padding:30px 30px 0;margin:60px 20px 0;background:#fff;font-size:14px}input{color:inherit;font:inherit;border:0;margin:0;outline:0;padding:0}.form-field{display:flex;margin-bottom:30px}.form-field input[type=password],.form-field input[type=text],.form-field input[type=submit]{width:100%;padding:1em}.form-field input[type=password],.form-field input[type=text]{border-radius:3px;border-width:1px;border-style:solid;border-color:#a6a6a6}.form-field input[type=submit]{background-color:#218fcf;color:#eee;font-weight:700;cursor:pointer;transition:all .3s}.form-field input[type=submit]:focus,.form-field input[type=submit]:hover{background-color:#1abfd3}.form-field input[type=submit]:active{transform:scale(.99)}.hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.alert{color:crimson}` + + // 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'; ` + + `style-src 'sha256-QC9ckaUFAdcN0Ysmu8q8iqCazYFgrJSQDJPa/przPXU='; ` + + `frame-ancestors 'none'` +) + +func TestTemplate(t *testing.T) { + const ( + testUpstreamName = "test-idp-name" + testPath = "test-post-path" + testEncodedState = "test-encoded-state" + testAlert = "test-alert-message" + ) + + var buf bytes.Buffer + pageInputs := &PageData{ + PostPath: testPath, + State: testEncodedState, + IDPName: testUpstreamName, + HasAlertError: true, + AlertMessage: testAlert, + } + + // Render with an alert. + expectedHTMLWithAlert := testutil.ExpectedLoginPageHTML(testExpectedCSS, testUpstreamName, testPath, testEncodedState, testAlert) + require.NoError(t, Template().Execute(&buf, pageInputs)) + // t.Logf("actual value:\n%s", buf.String()) // useful when updating minify library causes new output + require.Equal(t, expectedHTMLWithAlert, buf.String()) + + // Render again without an alert. + pageInputs.HasAlertError = false + expectedHTMLWithoutAlert := testutil.ExpectedLoginPageHTML(testExpectedCSS, testUpstreamName, testPath, testEncodedState, "") + buf = bytes.Buffer{} // clear previous result from buffer + require.NoError(t, Template().Execute(&buf, pageInputs)) + require.Equal(t, expectedHTMLWithoutAlert, buf.String()) +} + +func TestContentSecurityPolicy(t *testing.T) { + require.Equal(t, testExpectedCSP, ContentSecurityPolicy()) +} + +func TestCSS(t *testing.T) { + require.Equal(t, testExpectedCSS, CSS()) +} + +func TestHelpers(t *testing.T) { + require.Equal(t, "test", mustMinify("test", nil)) + require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) }) +} diff --git a/internal/oidc/provider/csp/csp.go b/internal/oidc/provider/csp/csp.go new file mode 100644 index 00000000..d3f97e50 --- /dev/null +++ b/internal/oidc/provider/csp/csp.go @@ -0,0 +1,15 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package csp defines helpers related to HTML Content Security Policies. +package csp + +import ( + "crypto/sha256" + "encoding/base64" +) + +func Hash(s string) string { + hashBytes := sha256.Sum256([]byte(s)) + return "sha256-" + base64.StdEncoding.EncodeToString(hashBytes[:]) +} diff --git a/internal/oidc/provider/csp/csp_test.go b/internal/oidc/provider/csp/csp_test.go new file mode 100644 index 00000000..746d5822 --- /dev/null +++ b/internal/oidc/provider/csp/csp_test.go @@ -0,0 +1,15 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package csp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHash(t *testing.T) { + // Example test vector from https://content-security-policy.com/hash/. + require.Equal(t, "sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=", Hash("doSomething();")) +} diff --git a/internal/oidc/provider/formposthtml/formposthtml.go b/internal/oidc/provider/formposthtml/formposthtml.go index 6552c9a1..b96f0d5d 100644 --- a/internal/oidc/provider/formposthtml/formposthtml.go +++ b/internal/oidc/provider/formposthtml/formposthtml.go @@ -6,13 +6,13 @@ package formposthtml import ( - "crypto/sha256" _ "embed" // Needed to trigger //go:embed directives below. - "encoding/base64" "html/template" "strings" "github.com/tdewolff/minify/v2/minify" + + "go.pinniped.dev/internal/oidc/provider/csp" ) var ( @@ -37,8 +37,8 @@ var parsedHTMLTemplate = template.Must(template.New("form_post.gohtml").Funcs(te // 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) + `'`, + `script-src '` + csp.Hash(minifiedJS) + `'`, + `style-src '` + csp.Hash(minifiedCSS) + `'`, `img-src data:`, `connect-src *`, `frame-ancestors 'none'`, @@ -51,14 +51,9 @@ func mustMinify(s string, err error) string { 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. +// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy. func ContentSecurityPolicy() string { return cspValue } // Template returns the html/template.Template for rendering the response_type=form_post response page. diff --git a/internal/oidc/provider/formposthtml/formposthtml_test.go b/internal/oidc/provider/formposthtml/formposthtml_test.go index 07fb508a..d5d69c9d 100644 --- a/internal/oidc/provider/formposthtml/formposthtml_test.go +++ b/internal/oidc/provider/formposthtml/formposthtml_test.go @@ -93,10 +93,6 @@ func TestContentSecurityPolicyHashes(t *testing.T) { } 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();")) } diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 3da0c2c3..ffa33139 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -139,7 +139,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler( upstreamStateEncoder, csrfCookieEncoder, - login.NewGetHandler(m.upstreamIDPs), + login.NewGetHandler(), login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage), ) diff --git a/internal/testutil/assertions.go b/internal/testutil/assertions.go index b592e07e..ee7bc2ed 100644 --- a/internal/testutil/assertions.go +++ b/internal/testutil/assertions.go @@ -54,7 +54,7 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets) } -func RequireSecurityHeadersWithFormPostCSPs(t *testing.T, response *httptest.ResponseRecorder) { +func RequireSecurityHeadersWithFormPostPageCSPs(t *testing.T, response *httptest.ResponseRecorder) { // Loosely confirm that the unique CSPs needed for the form_post page were used. cspHeader := response.Header().Get("Content-Security-Policy") require.Contains(t, cspHeader, "script-src '") // loose assertion @@ -66,8 +66,20 @@ func RequireSecurityHeadersWithFormPostCSPs(t *testing.T, response *httptest.Res requireSecurityHeaders(t, response) } -func RequireSecurityHeadersWithoutFormPostCSPs(t *testing.T, response *httptest.ResponseRecorder) { - // Confirm that the unique CSPs needed for the form_post page were NOT used. +func RequireSecurityHeadersWithLoginPageCSPs(t *testing.T, response *httptest.ResponseRecorder) { + // Loosely confirm that the unique CSPs needed for the login page were used. + cspHeader := response.Header().Get("Content-Security-Policy") + require.Contains(t, cspHeader, "style-src '") // loose assertion + require.NotContains(t, cspHeader, "script-src") // only needed by form_post page + require.NotContains(t, cspHeader, "img-src data:") // only needed by form_post page + require.NotContains(t, cspHeader, "connect-src *") // only needed by form_post page + + // Also require all the usual security headers. + requireSecurityHeaders(t, response) +} + +func RequireSecurityHeadersWithoutCustomCSPs(t *testing.T, response *httptest.ResponseRecorder) { + // Confirm that the unique CSPs needed for the form_post or login page were NOT used. cspHeader := response.Header().Get("Content-Security-Policy") require.NotContains(t, cspHeader, "script-src") require.NotContains(t, cspHeader, "style-src") @@ -79,7 +91,7 @@ func RequireSecurityHeadersWithoutFormPostCSPs(t *testing.T, response *httptest. } func requireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) { - // Loosely confirm that the generic CSPs were used. + // Loosely confirm that the generic default CSPs were used. cspHeader := response.Header().Get("Content-Security-Policy") require.Contains(t, cspHeader, "default-src 'none'") require.Contains(t, cspHeader, "frame-ancestors 'none'") diff --git a/internal/testutil/loginhtml.go b/internal/testutil/loginhtml.go new file mode 100644 index 00000000..431f708d --- /dev/null +++ b/internal/testutil/loginhtml.go @@ -0,0 +1,68 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package testutil + +import ( + "fmt" + + "go.pinniped.dev/internal/here" +) + +func ExpectedLoginPageHTML(wantCSS, wantIDPName, wantPostPath, wantEncodedState, wantAlert string) string { + alertHTML := "" + if wantAlert != "" { + alertHTML = fmt.Sprintf("\n"+ + "
\n"+ + " %s\n"+ + "
\n ", + wantAlert, + ) + } + + // Note that "role", "aria-*", and "alert" attributes are hints to screen readers. + // Also note that some structure and attributes used here are hints to password managers, + // see https://support.1password.com/compatible-website-design/. + // Please take care when changing the HTML of this form, + // and test with a screen reader and password manager after changes. + return here.Docf(` + + + Pinniped + + + + + +
+
+

Log in to %s

+
+ %s +
+ +
+ + +
+
+ + +
+
+ +
+
+
+ + + `, + wantCSS, + wantIDPName, + alertHTML, + wantPostPath, + wantEncodedState, + ) +} From 00d68845c4e148c66d93ed89b315827312a3d392 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 5 May 2022 13:42:23 -0700 Subject: [PATCH 41/77] Add `--flow` to choose login flow in prepare-supervisor-on-kind.sh --- hack/prepare-supervisor-on-kind.sh | 50 ++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 9c17d26c..6a573b1c 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -25,11 +25,36 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" +function log_error() { + RED='\033[0;31m' + NC='\033[0m' + if [[ ${COLORTERM:-unknown} =~ ^(truecolor|24bit)$ ]]; then + echo -e "🙁${RED} Error: $* ${NC}" + else + echo ":( Error: $*" + fi +} + use_oidc_upstream=no use_ldap_upstream=no use_ad_upstream=no +use_flow="" while (("$#")); do case "$1" in + --flow) + shift + # If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error + if [[ "$#" == "0" || "$1" == -* ]]; then + log_error "--flow requires a flow name to be specified (e.g. cli_password or browser_authcode" + exit 1 + fi + if [[ "$1" != "browser_authcode" && "$1" != "cli_password" ]]; then + log_error "--flow must be cli_password or browser_authcode" + exit 1 + fi + use_flow=$1 + shift + ;; --ldap) use_ldap_upstream=yes shift @@ -56,7 +81,7 @@ while (("$#")); do done if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" ]]; then - echo "Error: Please use --oidc, --ldap, or --ad to specify which type of upstream identity provider(s) you would like" + log_error "Error: Please use --oidc, --ldap, or --ad to specify which type of upstream identity provider(s) you would like" exit 1 fi @@ -127,6 +152,7 @@ spec: certificateAuthorityData: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE" authorizationConfig: additionalScopes: [ ${PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES} ] + allowPasswordGrant: true claims: username: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM" groups: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM" @@ -196,7 +222,7 @@ EOF fi if [[ "$use_ad_upstream" == "yes" ]]; then - # Make an ActiveDirectoryIdentityProvider. + # Make an ActiveDirectoryIdentityProvider. Needs to be pointed to a real AD server by env vars. cat <kubeconfig +flow_arg="" +if [[ -n "$use_flow" ]]; then + flow_arg="--upstream-identity-provider-flow $use_flow" +fi +https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped get kubeconfig --oidc-skip-browser $flow_arg >kubeconfig # Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login. rm -f "$HOME/.config/pinniped/sessions.yaml" @@ -265,25 +295,27 @@ rm -f "$HOME/.config/pinniped/credentials.yaml" echo echo "Ready! 🚀" -if [[ "$use_oidc_upstream" == "yes" ]]; then +if [[ "$use_oidc_upstream" == "yes" || "$use_flow" == "browser_authcode" ]]; then echo echo "To be able to access the login URL shown below, start Chrome like this:" echo " open -a \"Google Chrome\" --args --proxy-server=\"$PINNIPED_TEST_PROXY\"" - echo "Then use these credentials at the Dex login page:" + echo "Note that Chrome must be fully quit before being started with --proxy-server." + echo "Then open the login URL shown below in that new Chrome window." + echo + echo "When prompted for username and password, use these values:" +fi + +if [[ "$use_oidc_upstream" == "yes" ]]; then echo " Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME" echo " Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD" fi if [[ "$use_ldap_upstream" == "yes" ]]; then - echo - echo "When prompted for username and password by the CLI, use these values:" echo " Username: $PINNIPED_TEST_LDAP_USER_CN" echo " Password: $PINNIPED_TEST_LDAP_USER_PASSWORD" fi if [[ "$use_ad_upstream" == "yes" ]]; then - echo - echo "When prompted for username and password by the CLI, use these values:" echo " Username: $PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME" echo " Password: $PINNIPED_TEST_AD_USER_PASSWORD" fi From 6e6e1f4add06a40b1b8ac73f189c609572dcff03 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 5 May 2022 13:56:38 -0700 Subject: [PATCH 42/77] Update login page CSS selectors in e2e test --- test/integration/e2e_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index e4a73b66..ea5e0e50 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -1080,9 +1080,9 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo regex := regexp.MustCompile(`\A` + downstream.Spec.Issuer + `/login.+`) browsertest.WaitForURL(t, page, regex) - usernameSelector := "input#username" - passwordSelector := "input[type='password']" - loginButtonSelector := "button#submit" + usernameSelector := "#username" + passwordSelector := "#password" + loginButtonSelector := "#submit" browsertest.WaitForVisibleElements(t, page, usernameSelector, passwordSelector, loginButtonSelector) // Fill in the username and password and click "submit". From ec22b5715b35b5a684c8237bfae18f161fce8d5d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 5 May 2022 14:46:07 -0700 Subject: [PATCH 43/77] =?UTF-8?q?Add=20Pinniped=20favicon=20to=20login=20U?= =?UTF-8?q?I=20page=20=F0=9F=A6=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/oidc/login/loginhtml/login_form.gohtml | 14 ++++++++++++-- internal/testutil/loginhtml.go | 10 +++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/internal/oidc/login/loginhtml/login_form.gohtml b/internal/oidc/login/loginhtml/login_form.gohtml index a92e406f..15a13fcf 100644 --- a/internal/oidc/login/loginhtml/login_form.gohtml +++ b/internal/oidc/login/loginhtml/login_form.gohtml @@ -1,13 +1,23 @@ - Pinniped + Pinniped Login - +
diff --git a/internal/testutil/loginhtml.go b/internal/testutil/loginhtml.go index 431f708d..f46c55a3 100644 --- a/internal/testutil/loginhtml.go +++ b/internal/testutil/loginhtml.go @@ -20,18 +20,14 @@ func ExpectedLoginPageHTML(wantCSS, wantIDPName, wantPostPath, wantEncodedState, ) } - // Note that "role", "aria-*", and "alert" attributes are hints to screen readers. - // Also note that some structure and attributes used here are hints to password managers, - // see https://support.1password.com/compatible-website-design/. - // Please take care when changing the HTML of this form, - // and test with a screen reader and password manager after changes. return here.Docf(` - Pinniped + Pinniped Login - +
From 408e3900941b19f2f53578d885c0e0f4a355935b Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 6 May 2022 11:00:01 -0700 Subject: [PATCH 44/77] Add more detail on how we should display errors Signed-off-by: Margo Crawford --- proposals/1113_ldap-ad-web-ui/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 99571d58..6d8df473 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -106,8 +106,12 @@ logo and the IDP name. with Pinniped's form post html page, to be displayed on the login page. If it is `query`, return a redirect with the authcode as a query param. Default behavior when `response_mode` is unspecified should be handled by other parts of the code, but it should default to `query` on the supervisor. - 4. If the login fails, respond with an error so the login page can render an error message. Allow the user to retry - login the same way we do with the CLI today (we leave brute force protection to the IDP) + 4. If the login fails, respond with a redirect to `/login` with an error type as the query param, + so the login page can render an error message. Allow the user to retry login the same way we do with the CLI today + (we leave brute force protection to the IDP). Display two types of errors-- "login error" (incorrect username or password) + or "internal error" for something that can't be easily fixed by the user (for example, requests to the LDAP server timing + out, LDAP queries malformed). The error that is displayed to the user should be generic but should suggest to the user + whether they should try again, or contact their administrator. (thanks @vrabbi for the suggestion!) #### Upgrades From 4c44f583e9e38af74b541c7886f2e0538eb9fdf6 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 6 May 2022 12:00:46 -0700 Subject: [PATCH 45/77] Don't add pinniped_idp_name pinniped_idp_type params into upstream state --- internal/oidc/auth/auth_handler.go | 32 +++++++++++++++++++++---- internal/oidc/auth/auth_handler_test.go | 24 ++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 0c3df1e8..ae502d3a 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -7,6 +7,7 @@ package auth import ( "fmt" "net/http" + "net/url" "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" @@ -53,6 +54,12 @@ func NewHandler( return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method) } + // Note that the client might have used supervisoroidc.AuthorizeUpstreamIDPNameParamName and + // supervisoroidc.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP. + // The Pinniped CLI has been sending these params since v0.9.0. + // Currently, these are ignored because the Supervisor does not yet support logins when multiple IDPs + // are configured. However, these params should be honored in the future when choosing an upstream + // here, e.g. by calling supervisoroidc.FindUpstreamIDPByNameAndType() when the params are present. oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister) if err != nil { plog.WarningErr("authorize upstream config", err) @@ -65,7 +72,7 @@ func NewHandler( // The client set a username header, so they are trying to log in with a username/password. return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) } - return handleAuthRequestForOIDCUpstreamAuthcodeGrant(r, w, + return handleAuthRequestForOIDCUpstreamBrowserFlow(r, w, oauthHelperWithoutStorage, generateCSRF, generateNonce, generatePKCE, oidcUpstream, @@ -75,7 +82,7 @@ func NewHandler( ) } - // we know it's an AD/LDAP upstream. + // We know it's an AD/LDAP upstream. if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 || len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 { // The client set a username header, so they are trying to log in with a username/password. @@ -236,7 +243,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant( return nil } -func handleAuthRequestForOIDCUpstreamAuthcodeGrant( +func handleAuthRequestForOIDCUpstreamBrowserFlow( r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, @@ -487,7 +494,12 @@ func upstreamStateParam( encoder oidc.Encoder, ) (string, error) { stateParamData := oidc.UpstreamStateParamData{ - AuthParams: authorizeRequester.GetRequestForm().Encode(), + // The auth params might have included supervisoroidc.AuthorizeUpstreamIDPNameParamName and + // supervisoroidc.AuthorizeUpstreamIDPTypeParamName, but those can be ignored by other handlers + // that are reading from the encoded upstream state param being built here. + // The UpstreamName and UpstreamType struct fields can be used instead. + // Remove those params here to avoid potential confusion about which should be used later. + AuthParams: removeCustomIDPParams(authorizeRequester.GetRequestForm()).Encode(), UpstreamName: upstreamName, UpstreamType: upstreamType, Nonce: nonceValue, @@ -502,6 +514,18 @@ func upstreamStateParam( return encodedStateParamValue, nil } +func removeCustomIDPParams(params url.Values) url.Values { + p := url.Values{} + // Copy all params. + for k, v := range params { + p[k] = v + } + // Remove the unnecessary params. + delete(p, supervisoroidc.AuthorizeUpstreamIDPNameParamName) + delete(p, supervisoroidc.AuthorizeUpstreamIDPTypeParamName) + return p +} + func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken, codec oidc.Encoder) error { encodedCSRFValue, err := codec.Encode(oidc.CSRFCookieEncodingName, csrfValue) if err != nil { diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index e2b3f000..dc93c42a 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -847,8 +847,6 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), - contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, @@ -856,6 +854,24 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), nil), wantUpstreamStateParamInLocationHeader: true, }, + { + name: "OIDC upstream browser flow happy path with custom IDP name and type query params, which are excluded from the query params in the upstream state", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": "currently-ignored", "pinniped_idp_type": "oidc"}), + contentType: formContentType, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantBodyStringWithLocationInHref: true, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil), + wantUpstreamStateParamInLocationHeader: true, + }, { name: "OIDC upstream browser flow happy path with extra params that get passed through", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123", "def": "456"}).Build()), @@ -866,8 +882,6 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), - contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, @@ -885,8 +899,6 @@ func TestAuthorizationEndpoint(t *testing.T) { cookieEncoder: happyCookieEncoder, method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}), - contentType: formContentType, - body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusSeeOther, wantContentType: jsonContentType, wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery), From afc73221d6ea8fc9a77beb6c5d351a518d505053 Mon Sep 17 00:00:00 2001 From: Pinny Date: Fri, 6 May 2022 19:28:56 +0000 Subject: [PATCH 46/77] Updated versions in docs for v0.17.0 release --- site/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/config.yaml b/site/config.yaml index 24b01b75..ce02f523 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -7,7 +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.16.0 + latest_version: v0.17.0 latest_codegen_version: 1.23 pygmentsCodefences: true pygmentsStyle: "pygments" From 22aea6ab9d19db9aaca077904889a81fa161db47 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 9 May 2022 12:55:32 -0700 Subject: [PATCH 47/77] Address some small comments to make the doc more understandable Signed-off-by: Margo Crawford --- proposals/1113_ldap-ad-web-ui/README.md | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 6d8df473..d470647c 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -39,7 +39,7 @@ requiring each app to handle IDP credentials. * Provide generalized error messaging for failed logins that do not expose sensitive information (i.e. we should say "invalid username or password" but do not expose whether it's the username or password that's incorrect) * Provide information easily allowing a user to identify the screen as belonging to Pinniped and which upstream IdP is being represented (e.g. IdP name) -* Address basic security concerns for web firms (HTTPS, passwords use a password field, CSRF protection, redirect protection) +* Address basic security concerns for web forms (HTTPS, passwords use a password field, CSRF protection, redirect protection) * Prevent LDAP injection attacks * Rely on the upstream IdP to address advanced security concerns (brute force protection, username enumeration, etc) * Screens are accessible and friendly to screen readers @@ -47,7 +47,7 @@ requiring each app to handle IDP credentials. #### Non-goals * A rich client (ie the use of javascript) -* Advanced UI features (e.g. remember me, reveal password). These features are better left to identity providers to implement. +* Advanced UI features (e.g. remember me, reveal password). * Branding & customization beyond the information listed in the goals used to identify the login screen belongs to Pinniped. * Supporting SSO integrations * Internationalization or localization. The CLI doesn't currently support this either. @@ -81,8 +81,11 @@ Here is how the login flow might work: 1. The supervisor receives an authorization request. 1. If the client_id param is not "pinniped-cli", and it includes username and password via the custom headers, reject the request. 2. If the request does not include the custom username/password headers, assume we want to use the webpage login. - 3. Some day we may want to allow specifying the idp name and type as request parameters, but - for now we do not have multiple idps. + 3. Today, the CLI specifies the IDP name and type as request parameters, but the server currently ignores these + since the Supervisor does not allow multiple idps today. This could be enhanced in the future to use the requested + IDP when the params are present, and to show another UI page to allow the end user to choose which IDP when the params + are not present. This leaves room for future multiple IDP support in this flow, + however, the details are outside the scope of this proposal. 4. Encode the request parameters into a state param like is done today for the `OIDCIdentityProvider`. In addition to the values encoded today (auth params, upstream IDP name, nonce, csrf token and pkce), encode the upstream IDP type. @@ -150,9 +153,6 @@ With the pinniped cli: - succeeds with correct username and password - fails with incorrect username, shows useful but nonspecific error message - fails with incorrect password, shows useful but nonspecific error message -- with tty access, prompts for username and password on the cli -- without tty access, opens a browser -- without tty access, if the form post fails, don't ask user to copy and paste the authcode (we already know you have no tty to paste it into...) Once dynamic clients are implemented: - fails when attempting to pass username/password as headers on requests to the authorize endpoint - tests of the rest of the dynamic client functionality that should be detailed as part of that proposal @@ -164,7 +164,8 @@ The styling should match the [form post html page](https://github.com/vmware-tan as much as possible, we should reuse some of the existing css and add to it to keep the style consistent. #### Observability Considerations -* Logging login attempts at higher log levels. +* The existing logging in `upstreamldap.go` should be sufficient for logging the attempted logins. + Further logging should be proposed as a separate proposal. #### Security Considerations * Preventing LDAP injection attacks: this should be done server-side using our existing @@ -186,17 +187,22 @@ maintain both Pinniped and Dex in order to use this feature. It also means that users do not benefit from the opinionated `ActiveDirectoryIdentityProvider` config because Dex does not have an equivalent. +## Answered Questions +* Q: What is the format for the URL? (`issuer/some/path`? Something else?) + A: `/login` +* Q: Can we make it so we can reuse the existing cert, or will we need a new wildcard cert? + A: Since the page is hosted on the issuer, we can reuse the existing `FederationDomain` cert. + ## Open Questions -* What is the format for the URL? (`issuer/some/path`? Something else?) Can we make it so we can reuse the existing cert, - or will we need a new wildcard cert? -* Currently we have little validation on branding requirements. Is specifying the idp name enough for users to understand - how to log in? -* How many users will be blocked on using this feature until they can have a company name and logo on the login page? +* Q: Currently we have little validation on branding requirements. Is specifying the IDP name enough for users to understand + how to log in? How many users will be blocked on using this feature until they can have a company name and logo on the login page? + A: For our initial release, we will only specify the IDP name. We are open to adding further customization in response to feedback + from users once the feature is released. ## Implementation Plan While this work is intended to supplement the dynamic client work, parts of it can be implemented independently. -The pinniped cli can support a web based ui flow via a command line flag, environment variable or checking whether a tty is available. +The pinniped cli can support a web based ui flow via a command line flag, or environment variable. Then once dynamic clients exist, we can add functionality to accept requests from those clients as well. From 831abc315ee10fccf099fb68023e49342dbb2cf0 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 9 May 2022 14:45:18 -0700 Subject: [PATCH 48/77] Update audit log proposal key names and timestamp format --- proposals/1141_audit-logging/README.md | 39 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/proposals/1141_audit-logging/README.md b/proposals/1141_audit-logging/README.md index bc64a476..4ebe9cc9 100644 --- a/proposals/1141_audit-logging/README.md +++ b/proposals/1141_audit-logging/README.md @@ -59,7 +59,8 @@ Goals Non-goals -- Enabling Kubernetes API request auditing in the impersonation proxy. If needed, this will be handled in a separate feature. +- Enabling Kubernetes API request auditing in the impersonation proxy. If needed, this will be handled in a separate + feature. - Providing the ability to filter or choose which audit events to capture. - Auditing the management of CRs (e.g. OIDCIdentityProvider). These events are captured by the API server audit logs. @@ -170,6 +171,12 @@ sending to various destinations. ##### Audit Log JSON Format +The +[format of Kubernetes audit logs](https://github.com/kubernetes/kubernetes/blob/d0832102a7017e83bf47a5137b690e52f19c267c/staging/src/k8s.io/apiserver/pkg/apis/audit/v1/types.go#L72-L142) +is not a perfect fit for Pinniped. The Kubernetes audit logs are strongly oriented towards API requests for Kubernetes +resources, with many of the fields representing the details of a request and response. The format of the Pinniped audit +logs will draw inspiration from the Kubernetes audit events without trying to directly copy them. + Each line of audit log will represent an event. Each line will be a complete JSON object, i.e. `{"key1":"value1","key2":"value2"}`. @@ -194,11 +201,15 @@ Depending on the event type, an event might include other keys, such as: - `msg`: a freeform warning or error message meant to be read by a human (e.g. the error message that was returned by an upstream IDP during a failed login attempt) - `requestID`: a unique ID for the request, if the event is related to an API request -- `requestPath`: the path of the endpoint, if the event is related to an API request -- `requestorIP`: the client's IP, if the event is related to an API request -- `user`: the username of the user performing the action, if there is one -- `groups`: the group memberships of the user performing the action, if the action is related determining or changing - their group memberships +- `requestURI`: the path of the endpoint, if the event is related to an API request +- `verb`: the REST method called on the endpoint, if the event is related to an API request +- `sourceIPs`: the client's IPs, if the event is related to an API request +- `userAgent`: the user agent string reported by the client, if the event is related to an API request +- `user`: a nested structure which can include the `username`, `groups`, and `uid` of the user performing the action, if + there is one + +The names of many of these keys are purposefully similar to the names of the keys used by Kubernetes audit events to +make them feel familiar. The details of these additional keys will be worked out as the details of the specific events are being worked out, during implementation of this proposal. @@ -224,9 +235,11 @@ It would be desirable for a timestamp to: 4. Use at least millisecond precision 5. Use the consistent JSON key name `time` -[Syslog's RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.3) defines a timestamp format which meets -the above goals. An example timestamp in this format is `2003-10-11T22:14:15.003` which is represents UTC time on -October 11, 2003 at 10:14:15 pm, 3 milliseconds into the next second. +Golang's standard library's [interpretation](https://pkg.go.dev/time#pkg-constants) of RFC 3339 with nanosecond +precision defines a timestamp format which meets the above goals. An example timestamp in this format, printed +by `fmt.Println(time.Now().UTC().Format(time.RFC3339Nano))`, is `2022-05-09T21:32:59.811913Z`, which represents UTC time +on May 9, 2022, at 21:32:59 pm, 811913 nanoseconds into the next second. Note that trailing zeros on the nanoseconds are +dropped, so the length of the nanoseconds field is variable in the output. Given this timestamp format, the following fluentbit configuration could be used to parse Pinniped's audit logs. @@ -235,7 +248,7 @@ Given this timestamp format, the following fluentbit configuration could be used Name json Format json Time_Key time - Time_Format %Y-%m-%dT%H:%M:%S.%L + Time_Format %Y-%m-%dT%H:%M:%S.%LZ ``` #### Upgrades @@ -252,9 +265,9 @@ a GKE Ingress, if no health checks were configured. #### Tests -Audit logging will be a user-facing feature, and the format of the logs should be considered a documented and versioned API. -Unnecessary changes to the format should be avoided after the first release. Therefore, all audit log events should be -covered by unit tests. +Audit logging will be a user-facing feature, and the format of the logs should be considered a documented and versioned +API. Unnecessary changes to the format should be avoided after the first release. Therefore, all audit log events should +be covered by unit tests. This implies that it may be desirable for the implementation to involve passing around a pointer to some interface to all code which needs to add events to the audit log. Such an implementation would make the audit logs more testable. A From a4e32d8f3d922f115fda8450ace0db33a8287c49 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 9 May 2022 15:43:36 -0700 Subject: [PATCH 49/77] Extract browsertest.LoginToUpstreamLDAP() integration test helper --- test/integration/cli_test.go | 2 +- test/integration/e2e_test.go | 26 +++++------------ test/integration/supervisor_login_test.go | 2 +- test/integration/supervisor_warnings_test.go | 2 +- test/testlib/browsertest/browsertest.go | 30 ++++++++++++++++++-- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go index 5a8358ae..1e2d0357 100644 --- a/test/integration/cli_test.go +++ b/test/integration/cli_test.go @@ -338,7 +338,7 @@ func runPinnipedLoginOIDC( require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.CLIUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, page, env.CLIUpstreamOIDC) // Expect to be redirected to the localhost callback. t.Logf("waiting for redirect to callback") diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index ea5e0e50..b3fa7266 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -245,7 +245,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. t.Logf("waiting for response page %s", downstream.Spec.Issuer) @@ -358,7 +358,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. t.Logf("waiting for response page %s", downstream.Spec.Issuer) @@ -486,7 +486,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. t.Logf("waiting for response page %s", downstream.Spec.Issuer) @@ -965,7 +965,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo ) }) - // Add an OIDC upstream IDP and try using it to authenticate during kubectl commands. + // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands. t.Run("with Supervisor LDAP upstream IDP and browser flow", func(t *testing.T) { testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) @@ -1075,21 +1075,9 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo t.Logf("navigating to login page: %q", loginURL) require.NoError(t, page.Navigate(loginURL)) - // Expect to be redirected to the supervisor's ldap login page. - t.Logf("waiting for redirect to supervisor ldap login page") - regex := regexp.MustCompile(`\A` + downstream.Spec.Issuer + `/login.+`) - browsertest.WaitForURL(t, page, regex) - - usernameSelector := "#username" - passwordSelector := "#password" - loginButtonSelector := "#submit" - browsertest.WaitForVisibleElements(t, page, usernameSelector, passwordSelector, loginButtonSelector) - - // Fill in the username and password and click "submit". - t.Logf("logging into ldap") - require.NoError(t, page.First(usernameSelector).Fill(expectedUsername)) - require.NoError(t, page.First(passwordSelector).Fill(env.SupervisorUpstreamLDAP.TestUserPassword)) - require.NoError(t, page.First(loginButtonSelector).Click()) + // Confirm that we got to the login page, fill out the form, and submit the form. + browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer, + expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword) formpostExpectSuccessState(t, page) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 31089ec4..b849df2e 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -2005,7 +2005,7 @@ func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthor require.NoError(t, page.Navigate(downstreamAuthorizeURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) // Wait for the login to happen and us be redirected back to a localhost callback. t.Logf("waiting for redirect to callback") diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index 65fa06e1..f4ae43ef 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -448,7 +448,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) + browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. t.Logf("waiting for response page %s", downstream.Spec.Issuer) diff --git a/test/testlib/browsertest/browsertest.go b/test/testlib/browsertest/browsertest.go index d6686ea6..b04b1e1f 100644 --- a/test/testlib/browsertest/browsertest.go +++ b/test/testlib/browsertest/browsertest.go @@ -125,9 +125,9 @@ func WaitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) { ) } -// LoginToUpstream expects the page to be redirected to one of several known upstream IDPs. +// LoginToUpstreamOIDC expects the page to be redirected to one of several known upstream IDPs. // It knows how to enter the test username/password and submit the upstream login form. -func LoginToUpstream(t *testing.T, page *agouti.Page, upstream testlib.TestOIDCUpstream) { +func LoginToUpstreamOIDC(t *testing.T, page *agouti.Page, upstream testlib.TestOIDCUpstream) { t.Helper() type config struct { @@ -182,3 +182,29 @@ func LoginToUpstream(t *testing.T, page *agouti.Page, upstream testlib.TestOIDCU require.NoError(t, page.First(cfg.PasswordSelector).Fill(upstream.Password)) require.NoError(t, page.First(cfg.LoginButtonSelector).Click()) } + +// LoginToUpstreamLDAP expects the page to be redirected to the Supervisor's login UI for an LDAP/AD IDP. +// It knows how to enter the test username/password and submit the upstream login form. +func LoginToUpstreamLDAP(t *testing.T, page *agouti.Page, issuer, username, password string) { + t.Helper() + + usernameSelector := "#username" + passwordSelector := "#password" + loginButtonSelector := "#submit" + + loginURLRegexp, err := regexp.Compile(`\A` + regexp.QuoteMeta(issuer+"/login") + `.+\z`) + require.NoError(t, err) + + // Expect to be redirected to the login page. + t.Logf("waiting for redirect to %s/login page", issuer) + WaitForURL(t, page, loginURLRegexp) + + // Wait for the login page to be rendered. + WaitForVisibleElements(t, page, usernameSelector, passwordSelector, loginButtonSelector) + + // Fill in the username and password and click "submit". + t.Logf("logging in via Supervisor's upstream LDAP/AD login UI page") + require.NoError(t, page.First(usernameSelector).Fill(username)) + require.NoError(t, page.First(passwordSelector).Fill(password)) + require.NoError(t, page.First(loginButtonSelector).Click()) +} From ab302cf2b79e8d2ce4406d7af3ebcf04d053bf15 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 10 May 2022 10:30:32 -0700 Subject: [PATCH 50/77] Add AD via browser login e2e test and refactor e2e tests to share code --- test/integration/e2e_test.go | 396 ++++++++++++++++++----------------- 1 file changed, 201 insertions(+), 195 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index b3fa7266..998933d9 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -26,6 +26,7 @@ import ( coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/creack/pty" + "github.com/sclevine/agouti" "github.com/stretchr/testify/require" authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" @@ -57,7 +58,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo // Build pinniped CLI. pinnipedExe := testlib.PinnipedCLIPath(t) - tempDir := testutil.TempDir(t) // Infer the downstream issuer URL from the callback associated with the upstream test client registration. issuerURL, err := url.Parse(env.SupervisorUpstreamOIDC.CallbackURL) @@ -72,7 +72,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo require.NoError(t, err) // Save that bundle plus the one that signs the upstream issuer, for test purposes. - testCABundlePath := filepath.Join(tempDir, "test-ca.pem") + testCABundlePath := filepath.Join(testutil.TempDir(t), "test-ca.pem") testCABundlePEM := []byte(string(ca.Bundle()) + "\n" + env.SupervisorUpstreamOIDC.CABundle) testCABundleBase64 := base64.StdEncoding.EncodeToString(testCABundlePEM) require.NoError(t, ioutil.WriteFile(testCABundlePath, testCABundlePEM, 0600)) @@ -108,10 +108,12 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }) // Add an OIDC upstream IDP and try using it to authenticate during kubectl commands. - t.Run("with Supervisor OIDC upstream IDP and automatic flow", func(t *testing.T) { + t.Run("with Supervisor OIDC upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) { testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. page := browsertest.Open(t) @@ -149,7 +151,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }, idpv1alpha1.PhaseReady) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/oidc-test-sessions.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -162,89 +164,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }) // Run "kubectl get namespaces" which should trigger a browser login via the plugin. - start := time.Now() kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) - // Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an - // in-memory buffer, so we can have the full output available to us at the end. - originalStderrPipe, err := kubectlCmd.StderrPipe() - require.NoError(t, err) - originalStdoutPipe, err := kubectlCmd.StdoutPipe() - require.NoError(t, err) - var stderrPipeBuf, stdoutPipeBuf bytes.Buffer - stderrPipe := io.TeeReader(originalStderrPipe, &stderrPipeBuf) - stdoutPipe := io.TeeReader(originalStdoutPipe, &stdoutPipeBuf) + // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page) - t.Logf("starting kubectl subprocess") - require.NoError(t, kubectlCmd.Start()) - t.Cleanup(func() { - // Consume readers so that the tee buffers will contain all the output so far. - _, stdoutReadAllErr := readAllCtx(testCtx, stdoutPipe) - _, stderrReadAllErr := readAllCtx(testCtx, stderrPipe) - - // Note that Wait closes the stdout/stderr pipes, so we don't need to close them ourselves. - waitErr := kubectlCmd.Wait() - t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) - - // Upon failure, print the full output so far of the kubectl command. - var testAlreadyFailedErr error - if t.Failed() { - testAlreadyFailedErr = errors.New("test failed prior to clean up function") - } - cleanupErrs := utilerrors.NewAggregate([]error{waitErr, stdoutReadAllErr, stderrReadAllErr, testAlreadyFailedErr}) - - if cleanupErrs != nil { - t.Logf("kubectl stdout was:\n----start of stdout\n%s\n----end of stdout", stdoutPipeBuf.String()) - t.Logf("kubectl stderr was:\n----start of stderr\n%s\n----end of stderr", stderrPipeBuf.String()) - } - require.NoErrorf(t, cleanupErrs, "kubectl process did not exit cleanly and/or the test failed. "+ - "Note: if kubectl's first call to the Pinniped CLI results in the Pinniped CLI returning an error, "+ - "then kubectl may call the Pinniped CLI again, which may hang because it will wait for the user "+ - "to finish the login. This test will kill the kubectl process after a timeout. In this case, the "+ - " kubectl output printed above will include multiple prompts for the user to enter their authcode.", - ) - }) - - // Start a background goroutine to read stderr from the CLI and parse out the login URL. - loginURLChan := make(chan string, 1) - spawnTestGoroutine(testCtx, t, func() error { - reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderrPipe)) - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - loginURL, err := url.Parse(strings.TrimSpace(scanner.Text())) - if err == nil && loginURL.Scheme == "https" { - loginURLChan <- loginURL.String() // this channel is buffered so this will not block - return nil - } - } - return fmt.Errorf("expected stderr to contain login URL") - }) - - // Start a background goroutine to read stdout from kubectl and return the result as a string. - kubectlOutputChan := make(chan string, 1) - spawnTestGoroutine(testCtx, t, func() error { - output, err := readAllCtx(testCtx, stdoutPipe) - if err != nil { - return err - } - t.Logf("kubectl output:\n%s\n", output) - kubectlOutputChan <- string(output) // this channel is buffered so this will not block - return nil - }) - - // Wait for the CLI to print out the login URL and open the browser to it. - t.Logf("waiting for CLI to output login URL") - var loginURL string - select { - case <-time.After(1 * time.Minute): - require.Fail(t, "timed out waiting for login URL") - case loginURL = <-loginURLChan: - } - t.Logf("navigating to login page: %q", loginURL) - require.NoError(t, page.Navigate(loginURL)) - - // Expect to be redirected to the upstream provider and log in. + // Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form. browsertest.LoginToUpstreamOIDC(t, page, env.SupervisorUpstreamOIDC) // Expect to be redirected to the downstream callback which is serving the form_post HTML. @@ -255,17 +181,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo // It should now be in the "success" state. formpostExpectSuccessState(t, page) - // Expect the CLI to output a list of namespaces. - t.Logf("waiting for kubectl to output namespace list") - var kubectlOutput string - select { - case <-time.After(1 * time.Minute): - require.Fail(t, "timed out waiting for kubectl output") - case kubectlOutput = <-kubectlOutputChan: - } - requireKubectlGetNamespaceOutput(t, env, kubectlOutput) - - t.Logf("first kubectl command took %s", time.Since(start).String()) + requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, @@ -281,6 +197,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. page := browsertest.Open(t) @@ -318,7 +236,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }, idpv1alpha1.PhaseReady) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/oidc-test-sessions-manual.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", "--concierge-api-group-suffix", env.APIGroupSuffix, @@ -395,6 +314,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. page := browsertest.Open(t) @@ -440,7 +361,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }, idpv1alpha1.PhaseReady) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/oidc-test-sessions-manual.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", "--concierge-api-group-suffix", env.APIGroupSuffix, @@ -534,6 +456,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + expectedUsername := env.SupervisorUpstreamOIDC.Username expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups @@ -569,7 +493,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }, idpv1alpha1.PhaseReady) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/oidc-test-sessions-password-grant.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", "--concierge-api-group-suffix", env.APIGroupSuffix, @@ -620,6 +545,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + // Create upstream OIDC provider and wait for it to become ready. oidcIdentityProvider := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ Issuer: env.SupervisorUpstreamOIDC.Issuer, @@ -640,7 +567,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }, idpv1alpha1.PhaseReady) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/oidc-test-sessions-password-grant-negative-test.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", "--concierge-api-group-suffix", env.APIGroupSuffix, @@ -700,6 +628,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { t.Skip("LDAP integration test requires connectivity to an LDAP server") } @@ -710,7 +640,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/ldap-test-sessions.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -760,6 +690,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { t.Skip("LDAP integration test requires connectivity to an LDAP server") } @@ -770,7 +702,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/ldap-test-with-env-vars-sessions.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -832,6 +764,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { t.Skip("Active Directory integration test requires connectivity to an LDAP server") } @@ -845,7 +779,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/ad-test-sessions.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -895,6 +829,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { t.Skip("ActiveDirectory integration test requires connectivity to an LDAP server") } @@ -909,7 +845,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/ad-test-with-env-vars-sessions.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -965,11 +901,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo ) }) - // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands. - t.Run("with Supervisor LDAP upstream IDP and browser flow", func(t *testing.T) { + // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. + t.Run("with Supervisor LDAP upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) { testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. page := browsertest.Open(t) @@ -979,7 +917,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo setupClusterForEndToEndLDAPTest(t, expectedUsername, env) // Use a specific session cache for this test. - sessionCachePath := tempDir + "/ldap-test-sessions.yaml" + sessionCachePath := tempDir + "/test-sessions.yaml" kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ "get", "kubeconfig", @@ -993,105 +931,19 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo }) // Run "kubectl get namespaces" which should trigger a browser login via the plugin. - start := time.Now() kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) - // Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an - // in-memory buffer, so we can have the full output available to us at the end. - originalStderrPipe, err := kubectlCmd.StderrPipe() - require.NoError(t, err) - originalStdoutPipe, err := kubectlCmd.StdoutPipe() - require.NoError(t, err) - var stderrPipeBuf, stdoutPipeBuf bytes.Buffer - stderrPipe := io.TeeReader(originalStderrPipe, &stderrPipeBuf) - stdoutPipe := io.TeeReader(originalStdoutPipe, &stdoutPipeBuf) + // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page) - t.Logf("starting kubectl subprocess") - require.NoError(t, kubectlCmd.Start()) - t.Cleanup(func() { - // Consume readers so that the tee buffers will contain all the output so far. - _, stdoutReadAllErr := readAllCtx(testCtx, stdoutPipe) - _, stderrReadAllErr := readAllCtx(testCtx, stderrPipe) - - // Note that Wait closes the stdout/stderr pipes, so we don't need to close them ourselves. - waitErr := kubectlCmd.Wait() - t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) - - // Upon failure, print the full output so far of the kubectl command. - var testAlreadyFailedErr error - if t.Failed() { - testAlreadyFailedErr = errors.New("test failed prior to clean up function") - } - cleanupErrs := utilerrors.NewAggregate([]error{waitErr, stdoutReadAllErr, stderrReadAllErr, testAlreadyFailedErr}) - - if cleanupErrs != nil { - t.Logf("kubectl stdout was:\n----start of stdout\n%s\n----end of stdout", stdoutPipeBuf.String()) - t.Logf("kubectl stderr was:\n----start of stderr\n%s\n----end of stderr", stderrPipeBuf.String()) - } - require.NoErrorf(t, cleanupErrs, "kubectl process did not exit cleanly and/or the test failed. "+ - "Note: if kubectl's first call to the Pinniped CLI results in the Pinniped CLI returning an error, "+ - "then kubectl may call the Pinniped CLI again, which may hang because it will wait for the user "+ - "to finish the login. This test will kill the kubectl process after a timeout. In this case, the "+ - " kubectl output printed above will include multiple prompts for the user to enter their authcode.", - ) - }) - - // Start a background goroutine to read stderr from the CLI and parse out the login URL. - loginURLChan := make(chan string, 1) - spawnTestGoroutine(testCtx, t, func() error { - reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderrPipe)) - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - loginURL, err := url.Parse(strings.TrimSpace(scanner.Text())) - if err == nil && loginURL.Scheme == "https" { - loginURLChan <- loginURL.String() // this channel is buffered so this will not block - return nil - } - } - return fmt.Errorf("expected stderr to contain login URL") - }) - - // Start a background goroutine to read stdout from kubectl and return the result as a string. - kubectlOutputChan := make(chan string, 1) - spawnTestGoroutine(testCtx, t, func() error { - output, err := readAllCtx(testCtx, stdoutPipe) - if err != nil { - return err - } - t.Logf("kubectl output:\n%s\n", output) - kubectlOutputChan <- string(output) // this channel is buffered so this will not block - return nil - }) - - // Wait for the CLI to print out the login URL and open the browser to it. - t.Logf("waiting for CLI to output login URL") - var loginURL string - select { - case <-time.After(1 * time.Minute): - require.Fail(t, "timed out waiting for login URL") - case loginURL = <-loginURLChan: - } - t.Logf("navigating to login page: %q", loginURL) - require.NoError(t, page.Navigate(loginURL)) - - // Confirm that we got to the login page, fill out the form, and submit the form. + // Confirm that we got to the Supervisor's login page, fill out the form, and submit the form. browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer, expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword) formpostExpectSuccessState(t, page) - // Expect the CLI to output a list of namespaces. - t.Logf("waiting for kubectl to output namespace list") - var kubectlOutput string - select { - case <-time.After(1 * time.Minute): - require.Fail(t, "timed out waiting for kubectl output") - case kubectlOutput = <-kubectlOutputChan: - } - requireKubectlGetNamespaceOutput(t, env, kubectlOutput) - - t.Logf("first kubectl command took %s", time.Since(start).String()) + requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, downstream, @@ -1102,6 +954,160 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo expectedGroups, ) }) + + // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. + t.Run("with Supervisor Active Directory upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) { + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. + page := browsertest.Open(t) + + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("Active Directory integration test requires connectivity to an LDAP server") + } + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + + expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue + expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames + + setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/test-sessions.yaml" + + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-skip-browser", + "--oidc-ca-bundle", testCABundlePath, + "--upstream-identity-provider-flow", "browser_authcode", + "--oidc-session-cache", sessionCachePath, + }) + + // Run "kubectl get namespaces" which should trigger a browser login via the plugin. + kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + + // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page) + + // Confirm that we got to the Supervisor's login page, fill out the form, and submit the form. + browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer, + expectedUsername, env.SupervisorUpstreamActiveDirectory.TestUserPassword) + + formpostExpectSuccessState(t, page) + + requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) + }) +} + +func startKubectlAndOpenAuthorizationURLInBrowser(testCtx context.Context, t *testing.T, kubectlCmd *exec.Cmd, page *agouti.Page) chan string { + // Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an + // in-memory buffer, so we can have the full output available to us at the end. + originalStderrPipe, err := kubectlCmd.StderrPipe() + require.NoError(t, err) + originalStdoutPipe, err := kubectlCmd.StdoutPipe() + require.NoError(t, err) + var stderrPipeBuf, stdoutPipeBuf bytes.Buffer + stderrPipe := io.TeeReader(originalStderrPipe, &stderrPipeBuf) + stdoutPipe := io.TeeReader(originalStdoutPipe, &stdoutPipeBuf) + + t.Logf("starting kubectl subprocess") + require.NoError(t, kubectlCmd.Start()) + t.Cleanup(func() { + // Consume readers so that the tee buffers will contain all the output so far. + _, stdoutReadAllErr := readAllCtx(testCtx, stdoutPipe) + _, stderrReadAllErr := readAllCtx(testCtx, stderrPipe) + + // Note that Wait closes the stdout/stderr pipes, so we don't need to close them ourselves. + waitErr := kubectlCmd.Wait() + t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) + + // Upon failure, print the full output so far of the kubectl command. + var testAlreadyFailedErr error + if t.Failed() { + testAlreadyFailedErr = errors.New("test failed prior to clean up function") + } + cleanupErrs := utilerrors.NewAggregate([]error{waitErr, stdoutReadAllErr, stderrReadAllErr, testAlreadyFailedErr}) + + if cleanupErrs != nil { + t.Logf("kubectl stdout was:\n----start of stdout\n%s\n----end of stdout", stdoutPipeBuf.String()) + t.Logf("kubectl stderr was:\n----start of stderr\n%s\n----end of stderr", stderrPipeBuf.String()) + } + require.NoErrorf(t, cleanupErrs, "kubectl process did not exit cleanly and/or the test failed. "+ + "Note: if kubectl's first call to the Pinniped CLI results in the Pinniped CLI returning an error, "+ + "then kubectl may call the Pinniped CLI again, which may hang because it will wait for the user "+ + "to finish the login. This test will kill the kubectl process after a timeout. In this case, the "+ + " kubectl output printed above will include multiple prompts for the user to enter their authcode.", + ) + }) + + // Start a background goroutine to read stderr from the CLI and parse out the login URL. + loginURLChan := make(chan string, 1) + spawnTestGoroutine(testCtx, t, func() error { + reader := bufio.NewReader(testlib.NewLoggerReader(t, "stderr", stderrPipe)) + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + loginURL, err := url.Parse(strings.TrimSpace(scanner.Text())) + if err == nil && loginURL.Scheme == "https" { + loginURLChan <- loginURL.String() // this channel is buffered so this will not block + return nil + } + } + return fmt.Errorf("expected stderr to contain login URL") + }) + + // Start a background goroutine to read stdout from kubectl and return the result as a string. + kubectlOutputChan := make(chan string, 1) + spawnTestGoroutine(testCtx, t, func() error { + output, err := readAllCtx(testCtx, stdoutPipe) + if err != nil { + return err + } + t.Logf("kubectl output:\n%s\n", output) + kubectlOutputChan <- string(output) // this channel is buffered so this will not block + return nil + }) + + // Wait for the CLI to print out the login URL and open the browser to it. + t.Logf("waiting for CLI to output login URL") + var loginURL string + select { + case <-time.After(1 * time.Minute): + require.Fail(t, "timed out waiting for login URL") + case loginURL = <-loginURLChan: + } + t.Logf("navigating to login page: %q", loginURL) + require.NoError(t, page.Navigate(loginURL)) + + return kubectlOutputChan +} + +func waitForKubectlOutput(t *testing.T, kubectlOutputChan chan string) string { + t.Logf("waiting for kubectl output") + var kubectlOutput string + select { + case <-time.After(1 * time.Minute): + require.Fail(t, "timed out waiting for kubectl output") + case kubectlOutput = <-kubectlOutputChan: + } + return kubectlOutput } func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) { From 0b106c245e038de0d6f691e44a24ff207a22a535 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 10 May 2022 12:54:40 -0700 Subject: [PATCH 51/77] Add LDAP browser flow login test to supervisor_login_test.go --- test/integration/supervisor_login_test.go | 166 +++++++++++++++++----- test/testlib/browsertest/browsertest.go | 2 +- test/testlib/iotest.go | 4 +- 3 files changed, 136 insertions(+), 36 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index b849df2e..880ee0b3 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -47,7 +47,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { maybeSkip func(t *testing.T) createTestUser func(t *testing.T) (string, string) deleteTestUser func(t *testing.T, username string) - requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) + requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) createIDP func(t *testing.T) string wantDownstreamIDTokenSubjectToMatch string wantDownstreamIDTokenUsernameToMatch func(username string) string @@ -80,7 +80,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, idpv1alpha1.PhaseReady) return oidcIDP.Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { pinnipedSessionData := pinnipedSession.Custom pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" @@ -115,7 +115,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, idpv1alpha1.PhaseReady) return oidcIDP.Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Extra["username"] = "some-incorrect-username" @@ -169,7 +169,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, idpv1alpha1.PhaseReady) return oidcIDP.Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Extra["username"] = "some-incorrect-username" @@ -199,7 +199,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, idpv1alpha1.PhaseReady) return oidcIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamOIDC.Username, // username to present to server during login @@ -267,7 +267,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -301,6 +301,72 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, + { + name: "ldap with browser flow", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + }, + createIDP: func(t *testing.T) string { + t.Helper() + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) + ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), + }, + Bind: idpv1alpha1.LDAPIdentityProviderBind{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ + Base: env.SupervisorUpstreamLDAP.GroupSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: "dn", + }, + }, + }, idpv1alpha1.LDAPPhaseReady) + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) + return ldapIDP.Name + }, + createTestUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamLDAP.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ + "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), + ) + "$", + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, + }, { name: "ldap skip group refresh", maybeSkip: func(t *testing.T) { @@ -350,7 +416,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -440,7 +506,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -522,7 +588,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login @@ -596,7 +662,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -674,7 +740,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, time.Minute, 500*time.Millisecond) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -781,7 +847,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, time.Minute, 500*time.Millisecond) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -844,7 +910,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) return adIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login @@ -871,7 +937,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, - }, { + }, + { name: "activedirectory with custom options", maybeSkip: func(t *testing.T) { t.Helper() @@ -921,7 +988,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) return adIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login @@ -1003,7 +1070,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, time.Minute, 500*time.Millisecond) return adIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login @@ -1099,7 +1166,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, time.Minute, 500*time.Millisecond) return adIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login @@ -1168,7 +1235,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { deleteTestUser: func(t *testing.T, username string) { testlib.DeleteTestADUser(t, env, username) }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, testUserName, // username to present to server during login @@ -1230,7 +1297,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { deleteTestUser: func(t *testing.T, username string) { testlib.DeleteTestADUser(t, env, username) }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, testUserName, // username to present to server during login @@ -1292,7 +1359,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { deleteTestUser: func(t *testing.T, username string) { testlib.DeleteTestADUser(t, env, username) }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, testUserName, // username to present to server during login @@ -1348,7 +1415,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) return adIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login @@ -1409,7 +1476,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -1489,7 +1556,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, idpv1alpha1.LDAPPhaseReady) return ldapIDP.Name }, - requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, downstreamAuthorizeURL, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login @@ -1573,6 +1640,7 @@ func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv {"LDAPConnectionValid", "True", "Success"}, }, conditionsSummary) } + func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, expectedActiveDirectoryConnectionValidMessage string) { require.Len(t, adIDP.Status.Conditions, 4) @@ -1669,7 +1737,7 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes func testSupervisorLogin( t *testing.T, createIDP func(t *testing.T) string, - requestAuthorization func(t *testing.T, downstreamAuthorizeURL string, downstreamCallbackURL string, username string, password string, httpClient *http.Client), + requestAuthorization func(t *testing.T, downstreamIssuer string, downstreamAuthorizeURL string, downstreamCallbackURL string, username string, password string, httpClient *http.Client), editRefreshSessionDataWithoutBreaking func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string) []string, breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string), createTestUser func(t *testing.T) (string, string), @@ -1769,7 +1837,9 @@ func testSupervisorLogin( username, password := "", "" if createTestUser != nil { username, password = createTestUser(t) - defer deleteTestUser(t, username) + if deleteTestUser != nil { + defer deleteTestUser(t, username) + } } // Perform OIDC discovery for our downstream. @@ -1784,6 +1854,9 @@ func testSupervisorLogin( localCallbackServer := startLocalCallbackServer(t) // Form the OAuth2 configuration corresponding to our CLI client. + // Note that this is not using response_type=form_post, so the Supervisor will redirect to the callback endpoint + // directly, without using the Javascript form_post HTML page to POST back to the callback endpoint. The e2e + // tests which use the Pinniped CLI are testing the form_post part of the flow, so that is covered elsewhere. downstreamOAuth2Config := oauth2.Config{ // This is the hardcoded public client that the supervisor supports. ClientID: "pinniped-cli", @@ -1807,7 +1880,7 @@ func testSupervisorLogin( ) // Perform parameterized auth code acquisition. - requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient) + requestAuthorization(t, downstream.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient) // Expect that our callback handler was invoked. callback := localCallbackServer.waitForCallback(10 * time.Second) @@ -1984,7 +2057,7 @@ func verifyTokenResponse( require.True(t, strings.HasPrefix(tokenResponse.RefreshToken, "pin_rt_"), "token %q did not have expected prefix 'pin_rt_'", tokenResponse.RefreshToken) } -func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { +func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { t.Helper() env := testlib.IntegrationEnv(t) @@ -1992,12 +2065,7 @@ func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthor defer cancelFunc() // Make the authorize request once "manually" so we can check its response security headers. - authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) - require.NoError(t, err) - authorizeResp, err := httpClient.Do(authorizeRequest) - require.NoError(t, err) - require.NoError(t, authorizeResp.Body.Close()) - expectSecurityHeaders(t, authorizeResp, false) + makeAuthorizationRequestAndRequireSecurityHeaders(ctx, t, downstreamAuthorizeURL, httpClient) // Open the web browser and navigate to the downstream authorize URL. page := browsertest.Open(t) @@ -2013,6 +2081,38 @@ func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthor browsertest.WaitForURL(t, page, callbackURLPattern) } +func requestAuthorizationUsingBrowserAuthcodeFlowLDAP(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) { + t.Helper() + + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) + defer cancelFunc() + + // Make the authorize request once "manually" so we can check its response security headers. + makeAuthorizationRequestAndRequireSecurityHeaders(ctx, t, downstreamAuthorizeURL, httpClient) + + // Open the web browser and navigate to the downstream authorize URL. + page := browsertest.Open(t) + t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL)) + require.NoError(t, page.Navigate(downstreamAuthorizeURL)) + + // Expect to be redirected to the upstream provider and log in. + browsertest.LoginToUpstreamLDAP(t, page, downstreamIssuer, username, password) + + // Wait for the login to happen and us be redirected back to a localhost callback. + t.Logf("waiting for redirect to callback") + callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`) + browsertest.WaitForURL(t, page, callbackURLPattern) +} + +func makeAuthorizationRequestAndRequireSecurityHeaders(ctx context.Context, t *testing.T, downstreamAuthorizeURL string, httpClient *http.Client) { + authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) + require.NoError(t, err) + authorizeResp, err := httpClient.Do(authorizeRequest) + require.NoError(t, err) + require.NoError(t, authorizeResp.Body.Close()) + expectSecurityHeaders(t, authorizeResp, false) +} + func requestAuthorizationUsingCLIPasswordFlow(t *testing.T, downstreamAuthorizeURL, upstreamUsername, upstreamPassword string, httpClient *http.Client, wantErr bool) { t.Helper() diff --git a/test/testlib/browsertest/browsertest.go b/test/testlib/browsertest/browsertest.go index b04b1e1f..fa5325f9 100644 --- a/test/testlib/browsertest/browsertest.go +++ b/test/testlib/browsertest/browsertest.go @@ -112,7 +112,7 @@ func WaitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) { func(requireEventually *require.Assertions) { url, err := page.URL() if url != lastURL { - t.Logf("saw URL %s", url) + t.Logf("saw URL %s", testlib.MaskTokens(url)) lastURL = url } requireEventually.NoError(err) diff --git a/test/testlib/iotest.go b/test/testlib/iotest.go index 6f59f659..4837a165 100644 --- a/test/testlib/iotest.go +++ b/test/testlib/iotest.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testlib @@ -36,7 +36,7 @@ func (l *testlogReader) Read(p []byte) (n int, err error) { } // MaskTokens makes a best-effort attempt to mask out things that look like secret tokens in test output. -// The goal is more to have readable test output than for any security reason. +// Provides more readable test output, but also obscures sensitive state params and authcodes from public test output. func MaskTokens(in string) string { var tokenLike = regexp.MustCompile(`(?mi)[a-zA-Z0-9._-]{30,}|[a-zA-Z0-9]{20,}`) return tokenLike.ReplaceAllStringFunc(in, func(t string) string { From aa732a41fbff120f28cf51b18f510fce550a2112 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 10 May 2022 16:22:07 -0700 Subject: [PATCH 52/77] Add LDAP browser flow login failure tests to supervisor_login_test.go Also do some refactoring to share more common test setup code in supervisor_login_test.go. --- .../oidc/login/loginhtml/login_form.gohtml | 2 +- internal/testutil/loginhtml.go | 2 +- test/integration/e2e_test.go | 2 +- test/integration/supervisor_login_test.go | 1141 +++++------------ test/testlib/browsertest/browsertest.go | 34 +- 5 files changed, 382 insertions(+), 799 deletions(-) diff --git a/internal/oidc/login/loginhtml/login_form.gohtml b/internal/oidc/login/loginhtml/login_form.gohtml index 15a13fcf..c1ab8ba3 100644 --- a/internal/oidc/login/loginhtml/login_form.gohtml +++ b/internal/oidc/login/loginhtml/login_form.gohtml @@ -26,7 +26,7 @@ Notes:
{{if .HasAlertError}}
- {{.AlertMessage}} + {{.AlertMessage}}
{{end}}
diff --git a/internal/testutil/loginhtml.go b/internal/testutil/loginhtml.go index f46c55a3..0cf20bfa 100644 --- a/internal/testutil/loginhtml.go +++ b/internal/testutil/loginhtml.go @@ -14,7 +14,7 @@ func ExpectedLoginPageHTML(wantCSS, wantIDPName, wantPostPath, wantEncodedState, if wantAlert != "" { alertHTML = fmt.Sprintf("\n"+ "
\n"+ - " %s\n"+ + " %s\n"+ "
\n ", wantAlert, ) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 998933d9..0e16bde2 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -50,7 +50,7 @@ import ( ) // TestE2EFullIntegration_Browser tests a full integration scenario that combines the supervisor, concierge, and CLI. -func TestE2EFullIntegration_Browser(t *testing.T) { // nolint:gocyclo +func TestE2EFullIntegration_Browser(t *testing.T) { env := testlib.IntegrationEnv(t) topSetupCtx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 880ee0b3..3713175a 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "encoding/base64" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -38,10 +39,127 @@ import ( "go.pinniped.dev/test/testlib/browsertest" ) -// nolint:gocyclo func TestSupervisorLogin_Browser(t *testing.T) { env := testlib.IntegrationEnv(t) + skipNever := func(t *testing.T) { + // never need to skip this test + } + + skipLDAPTests := func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + } + + skipActiveDirectoryTests := func(t *testing.T) { + t.Helper() + skipLDAPTests(t) + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + } + + basicOIDCIdentityProviderSpec := func() idpv1alpha1.OIDCIdentityProviderSpec { + return idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, + Client: idpv1alpha1.OIDCClient{ + SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + }, + } + } + + createActiveDirectoryIdentityProvider := func(t *testing.T, edit func(spec *idpv1alpha1.ActiveDirectoryIdentityProviderSpec)) (*idpv1alpha1.ActiveDirectoryIdentityProvider, *v1.Secret) { + t.Helper() + + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + + spec := idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamActiveDirectory.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: secret.Name, + }, + } + + if edit != nil { + edit(&spec) + } + + adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, spec, idpv1alpha1.ActiveDirectoryPhaseReady) + + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + spec.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) + + return adIDP, secret + } + + createLDAPIdentityProvider := func(t *testing.T, edit func(spec *idpv1alpha1.LDAPIdentityProviderSpec)) (*idpv1alpha1.LDAPIdentityProvider, *v1.Secret) { + t.Helper() + + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) + + spec := idpv1alpha1.LDAPIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), + }, + Bind: idpv1alpha1.LDAPIdentityProviderBind{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ + Base: env.SupervisorUpstreamLDAP.GroupSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: "dn", + }, + }, + } + + if edit != nil { + edit(&spec) + } + + ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, spec, idpv1alpha1.LDAPPhaseReady) + + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + spec.Host, env.SupervisorUpstreamLDAP.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) + + return ldapIDP, secret + } + tests := []struct { name string maybeSkip func(t *testing.T) @@ -49,6 +167,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { deleteTestUser func(t *testing.T, username string) requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) createIDP func(t *testing.T) string + wantLocalhostCallbackToNeverHappen bool wantDownstreamIDTokenSubjectToMatch string wantDownstreamIDTokenUsernameToMatch func(username string) string wantDownstreamIDTokenGroups []string @@ -63,22 +182,10 @@ func TestSupervisorLogin_Browser(t *testing.T) { editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string }{ { - name: "oidc with default username and groups claim settings", - maybeSkip: func(t *testing.T) { - // never need to skip this test - }, + name: "oidc with default username and groups claim settings", + maybeSkip: skipNever, createIDP: func(t *testing.T) string { - t.Helper() - oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), - }, - Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, - }, - }, idpv1alpha1.PhaseReady) - return oidcIDP.Name + return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { @@ -91,29 +198,18 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, }, { - name: "oidc with custom username and groups claim settings", - maybeSkip: func(t *testing.T) { - // never need to skip this test - }, + name: "oidc with custom username and groups claim settings", + maybeSkip: skipNever, createIDP: func(t *testing.T) string { - t.Helper() - oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), - }, - Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, - }, - Claims: idpv1alpha1.OIDCClaims{ - Username: env.SupervisorUpstreamOIDC.UsernameClaim, - Groups: env.SupervisorUpstreamOIDC.GroupsClaim, - }, - AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, - }, - }, idpv1alpha1.PhaseReady) - return oidcIDP.Name + spec := basicOIDCIdentityProviderSpec() + spec.Claims = idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + } + spec.AuthorizationConfig = idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + } + return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { @@ -134,12 +230,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, }, { - name: "oidc without refresh token", - maybeSkip: func(t *testing.T) { - // never need to skip this test - }, + name: "oidc without refresh token", + maybeSkip: skipNever, createIDP: func(t *testing.T) string { - t.Helper() var additionalScopes []string // keep all the scopes except for offline access so we can test the access token based refresh flow. if len(env.ToolsNamespace) == 0 { @@ -151,23 +244,15 @@ func TestSupervisorLogin_Browser(t *testing.T) { } } } - oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), - }, - Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, - }, - Claims: idpv1alpha1.OIDCClaims{ - Username: env.SupervisorUpstreamOIDC.UsernameClaim, - Groups: env.SupervisorUpstreamOIDC.GroupsClaim, - }, - AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: additionalScopes, - }, - }, idpv1alpha1.PhaseReady) - return oidcIDP.Name + spec := basicOIDCIdentityProviderSpec() + spec.Claims = idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + } + spec.AuthorizationConfig = idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: additionalScopes, + } + return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { @@ -179,25 +264,14 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, { - name: "oidc with CLI password flow", - maybeSkip: func(t *testing.T) { - // never need to skip this test - }, + name: "oidc with CLI password flow", + maybeSkip: skipNever, createIDP: func(t *testing.T) string { - t.Helper() - oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), - }, - Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, - }, - AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ - AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider - }, - }, idpv1alpha1.PhaseReady) - return oidcIDP.Name + spec := basicOIDCIdentityProviderSpec() + spec.AuthorizationConfig = idpv1alpha1.OIDCAuthorizationConfig{ + AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider + } + return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -220,52 +294,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, }, { - name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -302,52 +335,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { - name: "ldap with browser flow", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap with browser flow", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name }, createTestUser: func(t *testing.T) (string, string) { // return the username and password of the existing user that we want to use for this test @@ -368,53 +360,68 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { - name: "ldap skip group refresh", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap with browser flow with wrong password", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - SkipGroupRefresh: true, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name + }, + createTestUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + "this is the wrong password" // password to present to server during login + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials, + wantLocalhostCallbackToNeverHappen: true, // we should have been sent back to the login page to retry login + }, + { + name: "ldap with browser flow with wrong username", + maybeSkip: skipLDAPTests, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name + }, + createTestUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return "this is the wrong username", // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials, + wantLocalhostCallbackToNeverHappen: true, // we should have been sent back to the login page to retry login + }, + { + name: "ldap with browser flow with wrong password and then correct password", + maybeSkip: skipLDAPTests, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name + }, + createTestUser: func(t *testing.T) (string, string) { + // return the username and password of the existing user that we want to use for this test + return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamLDAP.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ + "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), + ) + "$", + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, + }, + { + name: "ldap skip group refresh", + maybeSkip: skipLDAPTests, + createIDP: func(t *testing.T) string { + idp, _ := createLDAPIdentityProvider(t, func(spec *idpv1alpha1.LDAPIdentityProviderSpec) { + spec.GroupSearch.SkipGroupRefresh = true + }) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -454,10 +461,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { { name: "ldap with email as username and group search base that doesn't return anything, and using an LDAP provider which supports TLS", maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } + skipLDAPTests(t) if env.SupervisorUpstreamLDAP.UserSearchBase == env.SupervisorUpstreamLDAP.GroupSearchBase { // This test relies on using the user search base as the group search base, to simulate // searching for groups and not finding any. @@ -467,44 +471,10 @@ func TestSupervisorLogin_Browser(t *testing.T) { } }, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, // groups not stored at the user search base - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, func(spec *idpv1alpha1.LDAPIdentityProviderSpec) { + spec.GroupSearch.Base = env.SupervisorUpstreamLDAP.UserSearchBase // groups not stored at the user search base + }) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -541,52 +511,16 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: []string{}, }, { - name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.StartTLSOnlyHost, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "cn={}", // try using a non-default search filter - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: "dn", // try using the user's DN as the downstream username - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "cn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.StartTLSOnlyHost, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, func(spec *idpv1alpha1.LDAPIdentityProviderSpec) { + spec.Host = env.SupervisorUpstreamLDAP.StartTLSOnlyHost + spec.UserSearch.Filter = "cn={}" // try using a non-default search filter + spec.UserSearch.Attributes.Username = "dn" // try using the user's DN as the downstream username + spec.GroupSearch.Attributes.GroupName = "cn" + }) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -615,52 +549,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, }, { - name: "logging in to ldap with the wrong password fails", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "logging in to ldap with the wrong password fails", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -675,47 +568,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantErrorType: "access_denied", }, { - name: "ldap login still works after updating bind secret", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap login still works after updating bind secret", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { t.Helper() - - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - secretName := secret.Name - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secretName, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) + idp, secret := createLDAPIdentityProvider(t, nil) secret.Annotations = map[string]string{"pinniped.dev/test": "", "another-label": "another-key"} // update that secret, which will cause the cache to recheck tls and search base values @@ -734,11 +591,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.RequireEventually(t, func(requireEventually *require.Assertions) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - ldapIDP, err = supervisorClient.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace).Get(ctx, ldapIDP.Name, metav1.GetOptions{}) + idp, err = supervisorClient.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg) + requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, idp, expectedMsg) }, time.Minute, 500*time.Millisecond) - return ldapIDP.Name + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -768,53 +625,17 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { - name: "ldap login still works after deleting and recreating the bind secret", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap login still works after deleting and recreating the bind secret", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { t.Helper() - - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - secretName := secret.Name - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secretName, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) + idp, secret := createLDAPIdentityProvider(t, nil) // delete, then recreate that secret, which will cause the cache to recheck tls and search base values client := testlib.NewKubernetesClientset(t) deleteCtx, deleteCancel := context.WithTimeout(context.Background(), time.Minute) defer deleteCancel() - err := client.CoreV1().Secrets(env.SupervisorNamespace).Delete(deleteCtx, secretName, metav1.DeleteOptions{}) + err := client.CoreV1().Secrets(env.SupervisorNamespace).Delete(deleteCtx, secret.Name, metav1.DeleteOptions{}) require.NoError(t, err) // create the secret again @@ -822,7 +643,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { defer recreateCancel() recreatedSecret, err := client.CoreV1().Secrets(env.SupervisorNamespace).Create(recreateCtx, &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secret.Name, Namespace: env.SupervisorNamespace, }, Type: v1.SecretTypeBasicAuth, @@ -841,11 +662,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.RequireEventually(t, func(requireEventually *require.Assertions) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - ldapIDP, err = supervisorClient.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace).Get(ctx, ldapIDP.Name, metav1.GetOptions{}) + idp, err = supervisorClient.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg) + requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, idp, expectedMsg) }, time.Minute, 500*time.Millisecond) - return ldapIDP.Name + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -875,40 +696,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { - name: "activedirectory with all default options", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory with all default options", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secret.Name, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) - return adIDP.Name + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -939,54 +731,26 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, { - name: "activedirectory with custom options", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory with custom options", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{ + idp, _ := createActiveDirectoryIdentityProvider(t, func(spec *idpv1alpha1.ActiveDirectoryIdentityProviderSpec) { + spec.UserSearch = idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{ Base: env.SupervisorUpstreamActiveDirectory.UserSearchBase, Filter: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName + "={}", Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{ Username: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName, }, - }, - GroupSearch: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ + } + spec.GroupSearch = idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ Filter: "member={}", // excluding nested groups Base: env.SupervisorUpstreamActiveDirectory.GroupSearchBase, Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{ GroupName: "dn", }, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) - return adIDP.Name + } + }) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -1017,35 +781,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, }, { - name: "active directory login still works after updating bind secret", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory login still works after updating bind secret", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { t.Helper() - - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - secretName := secret.Name - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secretName, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) + idp, secret := createActiveDirectoryIdentityProvider(t, nil) secret.Annotations = map[string]string{"pinniped.dev/test": "", "another-label": "another-key"} // update that secret, which will cause the cache to recheck tls and search base values @@ -1064,11 +804,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.RequireEventually(t, func(requireEventually *require.Assertions) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - adIDP, err = supervisorClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace).Get(ctx, adIDP.Name, metav1.GetOptions{}) + idp, err = supervisorClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg) + requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, idp, expectedMsg) }, time.Minute, 500*time.Millisecond) - return adIDP.Name + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -1098,41 +838,17 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, { - name: "active directory login still works after deleting and recreating bind secret", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory login still works after deleting and recreating bind secret", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { t.Helper() - - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - secretName := secret.Name - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secretName, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) + idp, secret := createActiveDirectoryIdentityProvider(t, nil) // delete the secret client := testlib.NewKubernetesClientset(t) deleteCtx, deleteCancel := context.WithTimeout(context.Background(), time.Minute) defer deleteCancel() - err := client.CoreV1().Secrets(env.SupervisorNamespace).Delete(deleteCtx, secretName, metav1.DeleteOptions{}) + err := client.CoreV1().Secrets(env.SupervisorNamespace).Delete(deleteCtx, secret.Name, metav1.DeleteOptions{}) require.NoError(t, err) // create the secret again @@ -1140,7 +856,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { defer recreateCancel() recreatedSecret, err := client.CoreV1().Secrets(env.SupervisorNamespace).Create(recreateCtx, &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + Name: secret.Name, Namespace: env.SupervisorNamespace, }, Type: v1.SecretTypeBasicAuth, @@ -1160,11 +876,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.RequireEventually(t, func(requireEventually *require.Assertions) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - adIDP, err = supervisorClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace).Get(ctx, adIDP.Name, metav1.GetOptions{}) + idp, err = supervisorClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace).Get(ctx, idp.Name, metav1.GetOptions{}) requireEventually.NoError(err) - requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg) + requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, idp, expectedMsg) }, time.Minute, 500*time.Millisecond) - return adIDP.Name + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -1194,40 +910,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, { - name: "active directory login fails after the user password is changed", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory login fails after the user password is changed", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secret.Name, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) - return adIDP.Name + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name }, createTestUser: func(t *testing.T) (string, string) { return testlib.CreateFreshADTestUser(t, env) @@ -1256,40 +943,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: []string{}, // none for now. }, { - name: "active directory login fails after the user is deactivated", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory login fails after the user is deactivated", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secret.Name, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) - return adIDP.Name + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name }, createTestUser: func(t *testing.T) (string, string) { return testlib.CreateFreshADTestUser(t, env) @@ -1318,40 +976,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: []string{}, // none for now. }, { - name: "active directory login fails after the user is locked", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "active directory login fails after the user is locked", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secret.Name, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) - return adIDP.Name + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name }, createTestUser: func(t *testing.T) (string, string) { return testlib.CreateFreshADTestUser(t, env) @@ -1380,40 +1009,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: []string{}, }, { - name: "logging in to activedirectory with a deactivated user fails", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - }, + name: "logging in to active directory with a deactivated user fails", + maybeSkip: skipActiveDirectoryTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, - }, - ) - adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamActiveDirectory.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), - }, - Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ - SecretName: secret.Name, - }, - }, idpv1alpha1.ActiveDirectoryPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) - return adIDP.Name + idp, _ := createActiveDirectoryIdentityProvider(t, nil) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -1429,52 +1029,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantErrorType: "access_denied", }, { - name: "ldap refresh fails when username changes from email as username to dn as username", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap refresh fails when username changes from email as username to dn as username", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secret.Name, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - expectedMsg := fmt.Sprintf( - `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, - secret.Name, secret.ResourceVersion, - ) - requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -1513,48 +1072,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { - name: "ldap refresh updates groups to be empty after deleting the group search base", - maybeSkip: func(t *testing.T) { - t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - }, + name: "ldap refresh updates groups to be empty after deleting the group search base", + maybeSkip: skipLDAPTests, createIDP: func(t *testing.T) string { - t.Helper() - - secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, - map[string]string{ - v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, - v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, - }, - ) - secretName := secret.Name - ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), - }, - Bind: idpv1alpha1.LDAPIdentityProviderBind{ - SecretName: secretName, - }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ - Base: env.SupervisorUpstreamLDAP.UserSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, - }, - }, - GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ - Base: env.SupervisorUpstreamLDAP.GroupSearchBase, - Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "dn", - }, - }, - }, idpv1alpha1.LDAPPhaseReady) - return ldapIDP.Name + idp, _ := createLDAPIdentityProvider(t, nil) + return idp.Name }, requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) { requestAuthorizationUsingCLIPasswordFlow(t, @@ -1606,6 +1128,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { tt.breakRefreshSessionData, tt.createTestUser, tt.deleteTestUser, + tt.wantLocalhostCallbackToNeverHappen, tt.wantDownstreamIDTokenSubjectToMatch, tt.wantDownstreamIDTokenUsernameToMatch, tt.wantDownstreamIDTokenGroups, @@ -1742,6 +1265,7 @@ func testSupervisorLogin( breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string), createTestUser func(t *testing.T) (string, string), deleteTestUser func(t *testing.T, username string), + wantLocalhostCallbackToNeverHappen bool, wantDownstreamIDTokenSubjectToMatch string, wantDownstreamIDTokenUsernameToMatch func(username string) string, wantDownstreamIDTokenGroups []string, @@ -1883,7 +1407,17 @@ func testSupervisorLogin( requestAuthorization(t, downstream.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient) // Expect that our callback handler was invoked. - callback := localCallbackServer.waitForCallback(10 * time.Second) + callback, err := localCallbackServer.waitForCallback(10 * time.Second) + if wantLocalhostCallbackToNeverHappen { + require.Error(t, err) + // When we want the localhost callback to have never happened, then this is the end of the test. The login was + // unable to finish so there is nothing to assert about what should have happened with the callback, and there + // won't be any error sent to the callback either. + return + } + // Else, no error. + require.NoError(t, err) + t.Logf("got callback request: %s", testlib.MaskTokens(callback.URL.String())) if wantErrorType == "" { require.Equal(t, stateParam.String(), callback.URL.Query().Get("state")) @@ -2104,6 +1638,40 @@ func requestAuthorizationUsingBrowserAuthcodeFlowLDAP(t *testing.T, downstreamIs browsertest.WaitForURL(t, page, callbackURLPattern) } +func requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, _, username, password string, _ *http.Client) { + t.Helper() + + // Open the web browser and navigate to the downstream authorize URL. + page := browsertest.Open(t) + t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL)) + require.NoError(t, page.Navigate(downstreamAuthorizeURL)) + + // This functions assumes that it has been passed either a bad username or a bad password, and submits the + // provided credentials. Expect to be redirected to the upstream provider and attempt to log in. + browsertest.LoginToUpstreamLDAP(t, page, downstreamIssuer, username, password) + + // After failing login expect to land back on the login page again with an error message. + browsertest.WaitForUpstreamLDAPLoginPageWithError(t, page, downstreamIssuer) +} + +func requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, _, username, password string, _ *http.Client) { + t.Helper() + + // Open the web browser and navigate to the downstream authorize URL. + page := browsertest.Open(t) + t.Logf("opening browser to downstream authorize URL %s", testlib.MaskTokens(downstreamAuthorizeURL)) + require.NoError(t, page.Navigate(downstreamAuthorizeURL)) + + // Expect to be redirected to the upstream provider and attempt to log in. + browsertest.LoginToUpstreamLDAP(t, page, downstreamIssuer, username, "this is the wrong password!") + + // After failing login expect to land back on the login page again with an error message. + browsertest.WaitForUpstreamLDAPLoginPageWithError(t, page, downstreamIssuer) + + // Already at the login page, so this time can directly submit it using the provided username and password. + browsertest.SubmitUpstreamLDAPLoginForm(t, page, username, password) +} + func makeAuthorizationRequestAndRequireSecurityHeaders(ctx context.Context, t *testing.T, downstreamAuthorizeURL string, httpClient *http.Client) { authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) require.NoError(t, err) @@ -2191,13 +1759,12 @@ type localCallbackServer struct { callbacks <-chan *http.Request } -func (s *localCallbackServer) waitForCallback(timeout time.Duration) *http.Request { +func (s *localCallbackServer) waitForCallback(timeout time.Duration) (*http.Request, error) { select { case callback := <-s.callbacks: - return callback + return callback, nil case <-time.After(timeout): - require.Fail(s.t, "timed out waiting for callback request") - return nil + return nil, errors.New("timed out waiting for callback request") } } diff --git a/test/testlib/browsertest/browsertest.go b/test/testlib/browsertest/browsertest.go index fa5325f9..4a770b1a 100644 --- a/test/testlib/browsertest/browsertest.go +++ b/test/testlib/browsertest/browsertest.go @@ -188,11 +188,7 @@ func LoginToUpstreamOIDC(t *testing.T, page *agouti.Page, upstream testlib.TestO func LoginToUpstreamLDAP(t *testing.T, page *agouti.Page, issuer, username, password string) { t.Helper() - usernameSelector := "#username" - passwordSelector := "#password" - loginButtonSelector := "#submit" - - loginURLRegexp, err := regexp.Compile(`\A` + regexp.QuoteMeta(issuer+"/login") + `.+\z`) + loginURLRegexp, err := regexp.Compile(`\A` + regexp.QuoteMeta(issuer+"/login") + `\?state=.+\z`) require.NoError(t, err) // Expect to be redirected to the login page. @@ -200,11 +196,31 @@ func LoginToUpstreamLDAP(t *testing.T, page *agouti.Page, issuer, username, pass WaitForURL(t, page, loginURLRegexp) // Wait for the login page to be rendered. - WaitForVisibleElements(t, page, usernameSelector, passwordSelector, loginButtonSelector) + WaitForVisibleElements(t, page, "#username", "#password", "#submit") + + // Fill in the username and password and click "submit". + SubmitUpstreamLDAPLoginForm(t, page, username, password) +} + +func SubmitUpstreamLDAPLoginForm(t *testing.T, page *agouti.Page, username string, password string) { + t.Helper() // Fill in the username and password and click "submit". t.Logf("logging in via Supervisor's upstream LDAP/AD login UI page") - require.NoError(t, page.First(usernameSelector).Fill(username)) - require.NoError(t, page.First(passwordSelector).Fill(password)) - require.NoError(t, page.First(loginButtonSelector).Click()) + require.NoError(t, page.First("#username").Fill(username)) + require.NoError(t, page.First("#password").Fill(password)) + require.NoError(t, page.First("#submit").Click()) +} + +func WaitForUpstreamLDAPLoginPageWithError(t *testing.T, page *agouti.Page, issuer string) { + t.Helper() + + // Wait for redirect back to the login page again with an error. + t.Logf("waiting for redirect to back to login page with error message") + loginURLRegexp, err := regexp.Compile(`\A` + regexp.QuoteMeta(issuer+"/login") + `\?err=login_error&state=.+\z`) + require.NoError(t, err) + WaitForURL(t, page, loginURLRegexp) + + // Wait for the login page to be rendered again, this time also with an error message. + WaitForVisibleElements(t, page, "#username", "#password", "#submit", "#alert") } From 4101a55001544b79df41fd47a98737a17e76cdbc Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 11 May 2022 11:19:08 -0700 Subject: [PATCH 53/77] Update docs for new LDAP/AD browser-based login flow Also fix some comments that didn't fit onto one line in the yaml examples, be consistent about putting a blank line above `---` yaml separators, and some other small doc improvements. --- ...nfigure-supervisor-with-activedirectory.md | 24 +++++++---- .../howto/configure-supervisor-with-dex.md | 22 ++++++---- .../howto/configure-supervisor-with-gitlab.md | 5 +++ ...configure-supervisor-with-jumpcloudldap.md | 5 ++- .../howto/configure-supervisor-with-okta.md | 5 +++ .../configure-supervisor-with-openldap.md | 6 ++- ...re-supervisor-with-workspace_one_access.md | 21 ++++++---- .../docs/howto/configure-supervisor.md | 4 +- site/content/docs/howto/login.md | 42 ++++++++++++++----- .../docs/reference/code-walkthrough.md | 2 + 10 files changed, 98 insertions(+), 38 deletions(-) diff --git a/site/content/docs/howto/configure-supervisor-with-activedirectory.md b/site/content/docs/howto/configure-supervisor-with-activedirectory.md index 4b5aa387..0941f607 100644 --- a/site/content/docs/howto/configure-supervisor-with-activedirectory.md +++ b/site/content/docs/howto/configure-supervisor-with-activedirectory.md @@ -27,8 +27,8 @@ Create an [ActiveDirectoryIdentityProvider](https://github.com/vmware-tanzu/pinn ### ActiveDirectoryIdentityProvider with default options This ActiveDirectoryIdentityProvider uses all the default configuration options. - -Learn more about the default configuration [here]({{< ref "../reference/active-directory-configuration">}}) +The default configuration options are documented in the +[Active Directory configuration reference]({{< ref "../reference/active-directory-configuration">}}). ```yaml apiVersion: idp.supervisor.pinniped.dev/v1alpha1 @@ -41,14 +41,13 @@ spec: # Specify the host of the Active Directory server. host: "activedirectory.example.com:636" - # Specify the name of the Kubernetes Secret that contains your Active Directory - # bind account credentials. This service account will be used by the - # Supervisor to perform LDAP user and group searches. + # Specify the name of the Kubernetes Secret that contains your Active + # Directory bind account credentials. This service account will be + # used by the Supervisor to perform LDAP user and group searches. bind: secretName: "active-directory-bind-account" --- - apiVersion: v1 kind: Secret metadata: @@ -64,6 +63,10 @@ stringData: password: "YOUR_PASSWORD" ``` +Note that the `metadata.name` of the ActiveDirectoryIdentityProvider resource may be visible to end users at login prompts, +so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-active-directory` over `my-idp`. + If you've saved this into a file `activedirectory.yaml`, then install it into your cluster using: ```sh @@ -140,13 +143,16 @@ spec: # successful authentication. groupName: "dn" - # Specify the name of the Kubernetes Secret that contains your Active Directory - # bind account credentials. This service account will be used by the - # Supervisor to perform LDAP user and group searches. + # Specify the name of the Kubernetes Secret that contains your Active + # Directory bind account credentials. This service account will be + # used by the Supervisor to perform LDAP user and group searches. bind: secretName: "active-directory-bind-account" ``` +More information about the defaults for these configuration options can be found in +the [Active Directory configuration reference]({{< ref "../reference/active-directory-configuration">}}). + ## Next steps Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})! diff --git a/site/content/docs/howto/configure-supervisor-with-dex.md b/site/content/docs/howto/configure-supervisor-with-dex.md index 8c707653..fe549ca8 100644 --- a/site/content/docs/howto/configure-supervisor-with-dex.md +++ b/site/content/docs/howto/configure-supervisor-with-dex.md @@ -104,19 +104,21 @@ spec: # 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 + # Specify the name of the claim in your Dex ID token that represents the + # groups to which the user belongs. 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 + # 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 @@ -125,13 +127,19 @@ metadata: 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" + # The "Client ID" that you set in Dex. For example, in our case + # this is "pinniped-supervisor". clientID: "" - # The "Client secret" that you set in Dex. For example, in our case this is "pinniped-supervisor-secret" + # The "Client secret" that you set in Dex. For example, in our + # case this is "pinniped-supervisor-secret". clientSecret: "" ``` +Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts +if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-ldap` over `my-idp`. + Once your OIDCIdentityProvider resource has been created, you can validate your configuration by running: ```bash diff --git a/site/content/docs/howto/configure-supervisor-with-gitlab.md b/site/content/docs/howto/configure-supervisor-with-gitlab.md index d495d67b..75e08414 100644 --- a/site/content/docs/howto/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/configure-supervisor-with-gitlab.md @@ -89,6 +89,7 @@ spec: # application's client credentials (created below). client: secretName: gitlab-client-credentials + --- apiVersion: v1 kind: Secret @@ -105,6 +106,10 @@ stringData: clientSecret: "" ``` +Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts +if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-gitlab` over `my-idp`. + Once your OIDCIdentityProvider has been created, you can validate your configuration by running: ```shell diff --git a/site/content/docs/howto/configure-supervisor-with-jumpcloudldap.md b/site/content/docs/howto/configure-supervisor-with-jumpcloudldap.md index 732e9e77..7faa2e7c 100644 --- a/site/content/docs/howto/configure-supervisor-with-jumpcloudldap.md +++ b/site/content/docs/howto/configure-supervisor-with-jumpcloudldap.md @@ -120,7 +120,6 @@ spec: secretName: "jumpcloudldap-bind-account" --- - apiVersion: v1 kind: Secret metadata: @@ -138,6 +137,10 @@ stringData: password: "YOUR_PASSWORD" ``` +Note that the `metadata.name` of the LDAPIdentityProvider resource may be visible to end users at login prompts, +so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-ldap` over `my-idp`. + If you've saved this into a file `jumpcloud.yaml`, then install it into your cluster using: ```sh diff --git a/site/content/docs/howto/configure-supervisor-with-okta.md b/site/content/docs/howto/configure-supervisor-with-okta.md index 6e37573f..1c955b6f 100644 --- a/site/content/docs/howto/configure-supervisor-with-okta.md +++ b/site/content/docs/howto/configure-supervisor-with-okta.md @@ -97,6 +97,7 @@ spec: # application's client credentials (created below). client: secretName: okta-client-credentials + --- apiVersion: v1 kind: Secret @@ -113,6 +114,10 @@ stringData: clientSecret: "" ``` +Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts +if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-okta` over `my-idp`. + Once your OIDCIdentityProvider has been created, you can validate your configuration by running: ```shell diff --git a/site/content/docs/howto/configure-supervisor-with-openldap.md b/site/content/docs/howto/configure-supervisor-with-openldap.md index 35605024..aafb635f 100644 --- a/site/content/docs/howto/configure-supervisor-with-openldap.md +++ b/site/content/docs/howto/configure-supervisor-with-openldap.md @@ -158,6 +158,7 @@ spec: - name: certs secret: secretName: certs + --- apiVersion: v1 kind: Service @@ -265,7 +266,6 @@ spec: secretName: openldap-bind-account --- - apiVersion: v1 kind: Secret metadata: @@ -284,6 +284,10 @@ stringData: EOF ``` +Note that the `metadata.name` of the LDAPIdentityProvider resource may be visible to end users at login prompts, +so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-ldap` over `my-idp`. + Once your LDAPIdentityProvider has been created, you can validate your configuration by running: ```sh diff --git a/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md b/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md index a77c83ef..7d411169 100644 --- a/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md +++ b/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md @@ -76,7 +76,8 @@ spec: # the default claims in your token. The "openid" scope is always # included. # - # See the example claims below to learn how to customize the claims returned. + # See the example claims below to learn how to customize the + # claims returned. additionalScopes: [group, email] # Specify how Workspace ONE Access claims are mapped to Kubernetes identities. @@ -85,22 +86,22 @@ spec: # Specify the name of the claim in your Workspace ONE Access token that # will be mapped to the username in your Kubernetes environment. # - # User's emails can change. Use the sub claim if - # your environment requires a stable identifier. + # User's emails can change. Use the sub claim if your environment + # requires a stable identifier. username: email - # Specify the name of the claim in Workspace ONE Access that represents the - # groups the user belongs to. + # Specify the name of the claim in Workspace ONE Access that represents + # the groups to which the user belongs. # - # Group names may not be unique and can change. - # The group_ids claim is recommended for environments - # that want to use a more stable identifier. + # Group names may not be unique and can change. The group_ids claim is + # recommended for environments that want to use a more stable identifier. groups: group_names # Specify the name of the Kubernetes Secret that contains your # Workspace ONE Access application's client credentials (created below). client: secretName: ws1-client-credentials + --- apiVersion: v1 kind: Secret @@ -117,6 +118,10 @@ stringData: clientSecret: "" ``` +Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts +if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users. +For example, if you work at Acme Corp, choose something like `acme-corporate-workspace-one` over `my-idp`. + The following claims are returned by Workspace ONE Access. The `group` scope is required to use the `group_ids` and `group_names` claims. The `email` scope is required to use the `email` claim. The remaining claims are always available. diff --git a/site/content/docs/howto/configure-supervisor.md b/site/content/docs/howto/configure-supervisor.md index 5f2c7e47..baf998ad 100644 --- a/site/content/docs/howto/configure-supervisor.md +++ b/site/content/docs/howto/configure-supervisor.md @@ -244,6 +244,6 @@ should be signed by a certificate authority that is trusted by their browsers. ## Next steps Next, configure an OIDCIdentityProvider, ActiveDirectoryIdentityProvider, 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" >}}) +(several examples are available in these guides). Then +[configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}) on each cluster! diff --git a/site/content/docs/howto/login.md b/site/content/docs/howto/login.md index 45cfb3ad..b2ae46a0 100644 --- a/site/content/docs/howto/login.md +++ b/site/content/docs/howto/login.md @@ -72,6 +72,9 @@ pinniped get kubeconfig \ 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" >}}). +One flag of note is `--upstream-identity-provider-flow browser_authcode` to choose end-user `kubectl` login via a web browser +(the default for OIDCIdentityProviders), and `--upstream-identity-provider-flow cli_password` to choose end-user `kubectl` +login via CLI username/password prompts (the default for LDAPIdentityProviders and ActiveDirectoryIdentityProviders). ## Use the generated kubeconfig with `kubectl` to access the cluster @@ -94,20 +97,33 @@ 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, there are two supported client flows. +- For an OIDC identity provider, there are two supported client flows: - When using the default browser-based flow, `kubectl` will open the user's web browser and direct it to the login page of + 1. When using the default browser-based flow, `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. - When using the optional CLI-based flow, `kubectl` will interactively prompt the user for their username and password at the CLI. + 2. When using the optional CLI-based flow, `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. Note that the optional CLI-based flow must be enabled by the + administrator in the OIDCIdentityProvider configuration before use + (see `allowPasswordGrant` in the + [API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcauthorizationconfig) + for more details). + +- For LDAP and Active Directory identity providers, there are also two supported client flows: + + 1. When using the default CLI-based flow, `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. -- 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. + 2. When using the optional browser-based flow, `kubectl` will open the user's web browser and direct it to a login page + hosted by the Pinniped Supervisor. When the user enters their username and password, the Supervisor will authenticate + the user using the LDAP or Active Directory 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. + Unlike the optional flow for OIDC providers described above, this optional flow does not need to be configured in + the LDAPIdentityProvider or ActiveDirectoryIdentityProvider resource, so it is always available for end-users. 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. @@ -135,8 +151,14 @@ in the upstream identity provider, for example: --group auditors ``` -## Other notes +## Session and credential caching by the CLI -- 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). +Temporary session credentials such as ID, access, and refresh tokens are stored in: + - `$HOME/.config/pinniped/sessions.yaml` (macOS/Linux) + - `%USERPROFILE%/.config/pinniped/sessions.yaml` (Windows). + +Temporary cluster credentials such mTLS client certificates are stored in: + - `$HOME/.config/pinniped/credentials.yaml` (macOS/Linux) + - `%USERPROFILE%/.config/pinniped/credentials.yaml` (Windows). + +Deleting the contents of these directories is equivalent to performing a client-side logout. diff --git a/site/content/docs/reference/code-walkthrough.md b/site/content/docs/reference/code-walkthrough.md index de077f34..5ce69299 100644 --- a/site/content/docs/reference/code-walkthrough.md +++ b/site/content/docs/reference/code-walkthrough.md @@ -206,6 +206,8 @@ The per-FederationDomain endpoints are: See [internal/oidc/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/callback/callback_handler.go). - `/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers. See [internal/oidc/idpdiscovery/idp_discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/idpdiscovery/idp_discovery_handler.go). +- `/login` is a login UI page to support the optional browser-based login flow for LDAP and Active Directory identity providers. + See [internal/oidc/login/login_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/login/login_handler.go). The OIDC specifications implemented by the Supervisor can be found at [openid.net](https://openid.net/connect). From dc6874e9cd33f5c0a982fef86a70c933d52c8d66 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 16 May 2022 16:20:42 -0700 Subject: [PATCH 54/77] Move remaining open q's to answered q's --- proposals/1113_ldap-ad-web-ui/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index d470647c..510100ef 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -192,13 +192,14 @@ config because Dex does not have an equivalent. A: `/login` * Q: Can we make it so we can reuse the existing cert, or will we need a new wildcard cert? A: Since the page is hosted on the issuer, we can reuse the existing `FederationDomain` cert. - -## Open Questions * Q: Currently we have little validation on branding requirements. Is specifying the IDP name enough for users to understand how to log in? How many users will be blocked on using this feature until they can have a company name and logo on the login page? A: For our initial release, we will only specify the IDP name. We are open to adding further customization in response to feedback from users once the feature is released. +## Open Questions +None. + ## Implementation Plan While this work is intended to supplement the dynamic client work, parts of it can be implemented independently. From 1092fc4a9ef371651be8deda89c9dab7eef66506 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 16 May 2022 16:21:17 -0700 Subject: [PATCH 55/77] Add PR link to LDAP UI proposal --- proposals/1113_ldap-ad-web-ui/README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index 510100ef..ddb2fcbe 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -208,7 +208,4 @@ Then once dynamic clients exist, we can add functionality to accept requests from those clients as well. ## Implementation PRs -This section is a placeholder to list the PRs that implement this proposal. -This section should be left empty until after the proposal is approved. -After implementation, the proposal can be updated to list related -implementation PRs. +- https://github.com/vmware-tanzu/pinniped/pull/1163 From f008c081b3d844633ea4de483f20fc8c3aeb637e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 16 May 2022 16:21:33 -0700 Subject: [PATCH 56/77] Accept LDAP UI proposal --- proposals/1113_ldap-ad-web-ui/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/1113_ldap-ad-web-ui/README.md b/proposals/1113_ldap-ad-web-ui/README.md index ddb2fcbe..1a204c45 100644 --- a/proposals/1113_ldap-ad-web-ui/README.md +++ b/proposals/1113_ldap-ad-web-ui/README.md @@ -1,8 +1,8 @@ --- title: "Web UI for LDAP/AD login" authors: [ "@margocrawf" ] -status: "draft" -approval_date: "" +status: "accepted" +approval_date: "May 11, 2022" --- *Disclaimer*: Proposals are point-in-time designs and decisions. From cc985aa98a9232165ca9d6565a19699eb7966481 Mon Sep 17 00:00:00 2001 From: Anjali Telang Date: Thu, 19 May 2022 15:53:53 -0400 Subject: [PATCH 57/77] Roadmap updates for future Signed-off-by: Anjali Telang --- ROADMAP.md | 55 +++++++++++++----------------------------------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c80700de..20854e08 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,65 +2,36 @@ ## **Pinniped Project Roadmap** -### +### **About this document** -This document provides a link to the[ Pinniped Project issues](https://github.com/vmware-tanzu/pinniped/issues) list that serves as the up to date description of items that are in the Pinniped release pipeline. Most items are gathered from the community or include a feedback loop with the community. This should serve as a reference point for Pinniped users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan. +This document provides a high-level overview of the next big features the maintainers are planning to work on. This should serve as a reference point for Pinniped users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan. [Pinniped project backlog](https://github.com/orgs/vmware-tanzu/projects/43/) is prioritized based on this roadmap and it provides a more granular view of what the maintainers are working on a day-to-day basis. - -### +### **How to help?** -Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware-tanzu/pinniped/issues) or in [community meetings](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort. +Discussion on the roadmap can take place in [community meetings](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers). If you want to provide suggestions, use cases, and feedback to an item in the roadmap, please add them to the [meeting notes](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ) and we will discuss them during community meetings. Please review the roadmap to avoid potential duplicated effort. - -### -**Need an idea for a contribution?** - -We’ve created an [Opportunity Areas](https://github.com/vmware-tanzu/pinniped/discussions/483) discussion thread that outlines some areas we believe are excellent starting points for the community to get involved. In that discussion we’ve included specific work items that one might consider that also support the high-level items presented in our roadmap. - - -### +### **How to add an item to the roadmap?** -Please open an issue to track any initiative on the roadmap of Pinniped (usually driven by new feature requests). We will work with and rely on our community to focus our efforts to improve Pinniped. +One of the most important aspects in any open source community is the concept of proposals. Large changes to the codebase and / or new features should be preceded by a [proposal](https://github.com/vmware-tanzu/pinniped/tree/main/proposals) in our repo. +For smaller enhancements, you can open an issue to track that initiative or feature request. +We work with and rely on community feedback to focus our efforts to improve Pinniped and maintain a healthy roadmap. -### +### **Current Roadmap** The following table includes the current roadmap for Pinniped. If you have any questions or would like to contribute to Pinniped, please attend a [community meeting](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers) to discuss with our team. If you don't know where to start, we are always looking for contributors that will help us reduce technical, automation, and documentation debt. Please take the timelines & dates as proposals and goals. Priorities and requirements change based on community feedback, roadblocks encountered, community contributions, etc. If you depend on a specific item, we encourage you to attend community meetings to get updated status information, or help us deliver that feature by contributing to Pinniped. -Last Updated: March 2022 +Last Updated: May 2022 |Theme|Description|Timeline| |--|--|--| |Improving Security Posture|Support Audit logging of security events related to Authentication |May/June 2022| |Improving Usability|Support for integrating with UI/Dashboards |May/June 2022| -|Improving Security Posture|TLS hardening contd|June/July 2022| -|Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Exploring/Ongoing| -|Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing| -|Improving Security Posture|Key management/rotation for Pinniped components with minimal downtime |Exploring/Ongoing| -|Improving Security Posture|Support for Session Logout |Exploring/Ongoing| -|Improving Security Posture|Support for Idle Session/ Inactivity timeout|Exploring/Ongoing| -|Improving Security Posture|Support for Max Concurrent Sessions|Exploring/Ongoing| -|Improving Security Posture|Support for configurable Session Length |Exploring/Ongoing| -|Improving Security Posture|Reject use of username and groups with system: prefix |Exploring/Ongoing| -|Improving Security Posture|Support for using external KMS for Supervisor signing keys |Exploring/Ongoing| -|Improving Security Posture|Client side use of Secure Enclaves for Session data |Exploring/Ongoing| -|Improving Security Posture|Enforce the use of HTTP Strict Transport (HSTS) |Exploring/Ongoing| -|Improving Security Posture|Assert that Pinniped runs under the restricted PSP version2 levels |Exploring/Ongoing| -|Wider Concierge cluster support|Support for OpenShift cluster types in the Concierge|Exploring/Ongoing| -|Identity transforms|Support prefixing, filtering, or performing coarse-grained checks on upstream users and groups|Exploring/Ongoing| -|CLI SSO|Support Kerberos based authentication on CLI |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| -|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| -|CLI Improvements|Improving CLI UX for setting up Supervisor IDPs|Exploring/Ongoing| -|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| - - +|Improving Security Posture| Secrets Rotation and Management |Q3 2022| +|Improving Security Posture|Session Management |Q4 2022| +|Improving Security Posture|TLS hardening contd|Q4 2022| From 39fd9ba2701477db1f8b1032e1fb6fe8c8f87a15 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 19 May 2022 16:02:08 -0700 Subject: [PATCH 58/77] Small refactors and comments for LDAP/AD UI --- cmd/pinniped/cmd/kubeconfig.go | 4 +- cmd/pinniped/cmd/kubeconfig_test.go | 3 +- internal/oidc/auth/auth_handler.go | 58 ++++++++++++------- internal/oidc/login/get_login_handler.go | 4 +- internal/oidc/login/get_login_handler_test.go | 2 +- internal/oidc/login/login_handler.go | 3 + internal/oidc/login/loginhtml/loginhtml.go | 6 +- .../oidc/login/loginhtml/loginhtml_test.go | 4 +- internal/oidc/oidc.go | 3 + .../provider/formposthtml/formposthtml.go | 6 +- .../formposthtml/formposthtml_test.go | 4 +- internal/oidc/provider/manager/manager.go | 5 +- ...re-supervisor-with-workspace_one_access.md | 4 -- 13 files changed, 64 insertions(+), 42 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 74280ec5..e46eebde 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -922,7 +922,9 @@ func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, se return discoveredIDPFlows[0], nil default: // The user did not specify a flow, and more than one was found. - log.Info("multiple client flows found, selecting first value as default: "+discoveredIDPFlows[0].String(), "idpName", selectedIDPName, "idpType", selectedIDPType) + log.Info("multiple client flows found, selecting first value as default", + "idpName", selectedIDPName, "idpType", selectedIDPType, + "selectedFlow", discoveredIDPFlows[0].String(), "availableFlows", discoveredIDPFlows) return discoveredIDPFlows[0], nil } } diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index e5c27797..76a216f0 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -1305,7 +1305,8 @@ func TestGetKubeconfig(t *testing.T) { base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, wantLogs: func(_ string, _ string) []string { - return []string{"\"level\"=0 \"msg\"=\"multiple client flows found, selecting first value as default: cli_password\" \"idpName\"=\"some-ldap-idp\" \"idpType\"=\"ldap\""} + return []string{`"level"=0 "msg"="multiple client flows found, selecting first value as default" ` + + `"availableFlows"=["cli_password","flow2"] "idpName"="some-ldap-idp" "idpType"="ldap" "selectedFlow"="cli_password"`} }, }, { diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index ae502d3a..b4b9fccd 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -159,7 +159,7 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, ) error { - encodedStateParamValue, _, _, err := handleBrowserAuthRequest( + authRequestState, err := handleBrowserFlowAuthRequest( r, w, oauthHelper, @@ -174,11 +174,12 @@ func handleAuthRequestForLDAPUpstreamBrowserFlow( if err != nil { return err } - if encodedStateParamValue == "" { + if authRequestState == nil { + // There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it. return nil } - return login.RedirectToLoginPage(r, w, downstreamIssuer, encodedStateParamValue, login.ShowNoError) + return login.RedirectToLoginPage(r, w, downstreamIssuer, authRequestState.encodedStateParam, login.ShowNoError) } func handleAuthRequestForOIDCUpstreamPasswordGrant( @@ -255,7 +256,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, ) error { - encodedStateParamValue, pkceValue, nonceValue, err := handleBrowserAuthRequest( + authRequestState, err := handleBrowserFlowAuthRequest( r, w, oauthHelper, @@ -270,7 +271,8 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( if err != nil { return err } - if encodedStateParamValue == "" { + if authRequestState == nil { + // There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it. return nil } @@ -284,9 +286,9 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( } authCodeOptions := []oauth2.AuthCodeOption{ - nonceValue.Param(), - pkceValue.Challenge(), - pkceValue.Method(), + authRequestState.nonce.Param(), + authRequestState.pkce.Challenge(), + authRequestState.pkce.Method(), } for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { @@ -295,7 +297,7 @@ func handleAuthRequestForOIDCUpstreamBrowserFlow( http.Redirect(w, r, upstreamOAuthConfig.AuthCodeURL( - encodedStateParamValue, + authRequestState.encodedStateParam, authCodeOptions..., ), http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 @@ -387,10 +389,21 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider } } -// handleBrowserAuthRequest performs the shared validations and setup between browser based auth requests -// regardless of IDP type-- LDAP, Active Directory and OIDC. +type browserFlowAuthRequestState struct { + encodedStateParam string + pkce pkce.Code + nonce nonce.Nonce +} + +// handleBrowserFlowAuthRequest performs the shared validations and setup between browser based +// auth requests regardless of IDP type-- LDAP, Active Directory and OIDC. // It generates the state param, sets the CSRF cookie, and validates the prompt param. -func handleBrowserAuthRequest( +// It returns an error when it encounters an error without handling it, leaving it to +// the caller to decide how to handle it. +// It returns nil with no error when it encounters an error and also has already handled writing +// the error response to the ResponseWriter, in which case the caller should not also try to +// write the error response. +func handleBrowserFlowAuthRequest( r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, @@ -401,10 +414,10 @@ func handleBrowserAuthRequest( idpType psession.ProviderType, cookieCodec oidc.Codec, upstreamStateEncoder oidc.Encoder, -) (string, pkce.Code, nonce.Nonce, error) { +) (*browserFlowAuthRequestState, error) { authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) if !created { - return "", "", "", nil + return nil, nil // already wrote the error response, don't return error } now := time.Now() @@ -420,13 +433,13 @@ func handleBrowserAuthRequest( }) if err != nil { oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, false) - return "", "", "", nil + return nil, nil // already wrote the error response, don't return error } csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) if err != nil { plog.Error("authorize generate error", err) - return "", "", "", err + return nil, err } csrfFromCookie := readCSRFCookie(r, cookieCodec) if csrfFromCookie != "" { @@ -444,13 +457,13 @@ func handleBrowserAuthRequest( ) if err != nil { plog.Error("authorize upstream state param error", err) - return "", "", "", err + return nil, err } promptParam := r.Form.Get(promptParamName) if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false) - return "", "", "", nil + return nil, nil // already wrote the error response, don't return error } if csrfFromCookie == "" { @@ -458,10 +471,15 @@ func handleBrowserAuthRequest( err = addCSRFSetCookieHeader(w, csrfValue, cookieCodec) if err != nil { plog.Error("error setting CSRF cookie", err) - return "", "", "", err + return nil, err } } - return encodedStateParamValue, pkceValue, nonceValue, nil + + return &browserFlowAuthRequestState{ + encodedStateParam: encodedStateParamValue, + pkce: pkceValue, + nonce: nonceValue, + }, nil } func generateValues( diff --git a/internal/oidc/login/get_login_handler.go b/internal/oidc/login/get_login_handler.go index 3e33c937..d6da85a6 100644 --- a/internal/oidc/login/get_login_handler.go +++ b/internal/oidc/login/get_login_handler.go @@ -15,12 +15,12 @@ const ( incorrectUsernameOrPasswordErrorMessage = "Incorrect username or password." ) -func NewGetHandler() HandlerFunc { +func NewGetHandler(loginPath string) HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { alertMessage, hasAlert := getAlert(r) pageInputs := &loginhtml.PageData{ - PostPath: r.URL.Path, // the path for POST is the same as for GET + PostPath: loginPath, State: encodedState, IDPName: decodedState.UpstreamName, HasAlertError: hasAlert, diff --git a/internal/oidc/login/get_login_handler_test.go b/internal/oidc/login/get_login_handler_test.go index 472148d5..bb85b8f2 100644 --- a/internal/oidc/login/get_login_handler_test.go +++ b/internal/oidc/login/get_login_handler_test.go @@ -96,7 +96,7 @@ func TestGetLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - handler := NewGetHandler() + handler := NewGetHandler(testPath) target := testPath + "?state=" + tt.encodedState if tt.errParam != "" { target += "&err=" + tt.errParam diff --git a/internal/oidc/login/login_handler.go b/internal/oidc/login/login_handler.go index 06444bc1..1b358f2b 100644 --- a/internal/oidc/login/login_handler.go +++ b/internal/oidc/login/login_handler.go @@ -94,6 +94,9 @@ func wrapSecurityHeaders(handler http.Handler) http.Handler { }) } +// RedirectToLoginPage redirects to the GET /login page of the specified issuer. +// The specified issuer should never end with a "/", which is validated by +// provider.FederationDomainIssuer when the issuer string comes from that type. func RedirectToLoginPage( r *http.Request, w http.ResponseWriter, diff --git a/internal/oidc/login/loginhtml/loginhtml.go b/internal/oidc/login/loginhtml/loginhtml.go index 1493979f..2cd97c57 100644 --- a/internal/oidc/login/loginhtml/loginhtml.go +++ b/internal/oidc/login/loginhtml/loginhtml.go @@ -18,7 +18,7 @@ import ( var ( //go:embed login_form.css rawCSS string - minifiedCSS = mustMinify(minify.CSS(rawCSS)) + minifiedCSS = panicOnError(minify.CSS(rawCSS)) //go:embed login_form.gohtml rawHTMLTemplate string @@ -26,7 +26,7 @@ var ( // Parse the Go templated HTML and inject functions providing the minified inline CSS and JS. var parsedHTMLTemplate = template.Must(template.New("login_form.gohtml").Funcs(template.FuncMap{ - "minifiedCSS": func() template.CSS { return template.CSS(minifiedCSS) }, + "minifiedCSS": func() template.CSS { return template.CSS(CSS()) }, }).Parse(rawHTMLTemplate)) // Generate the CSP header value once since it's effectively constant. @@ -36,7 +36,7 @@ var cspValue = strings.Join([]string{ `frame-ancestors 'none'`, }, "; ") -func mustMinify(s string, err error) string { +func panicOnError(s string, err error) string { if err != nil { panic(err) } diff --git a/internal/oidc/login/loginhtml/loginhtml_test.go b/internal/oidc/login/loginhtml/loginhtml_test.go index a2e91ed1..50d8dc95 100644 --- a/internal/oidc/login/loginhtml/loginhtml_test.go +++ b/internal/oidc/login/loginhtml/loginhtml_test.go @@ -63,6 +63,6 @@ func TestCSS(t *testing.T) { } func TestHelpers(t *testing.T) { - require.Equal(t, "test", mustMinify("test", nil)) - require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) }) + require.Equal(t, "test", panicOnError("test", nil)) + require.PanicsWithError(t, "some error", func() { panicOnError("", fmt.Errorf("some error")) }) } diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index b45e757a..79380df7 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -43,6 +43,9 @@ const ( // Just in case we need to make a breaking change to the format of the upstream state param, // we are including a format version number. This gives the opportunity for a future version of Pinniped // to have the consumer of this format decide to reject versions that it doesn't understand. + // + // Version 1 was the original version. + // Version 2 added the UpstreamType field to the UpstreamStateParamData struct. UpstreamStateParamFormatVersion = "2" // The `name` passed to the encoder for encoding the upstream state param value. This name is short diff --git a/internal/oidc/provider/formposthtml/formposthtml.go b/internal/oidc/provider/formposthtml/formposthtml.go index b96f0d5d..d1a26c34 100644 --- a/internal/oidc/provider/formposthtml/formposthtml.go +++ b/internal/oidc/provider/formposthtml/formposthtml.go @@ -18,11 +18,11 @@ import ( var ( //go:embed form_post.css rawCSS string - minifiedCSS = mustMinify(minify.CSS(rawCSS)) + minifiedCSS = panicOnError(minify.CSS(rawCSS)) //go:embed form_post.js rawJS string - minifiedJS = mustMinify(minify.JS(rawJS)) + minifiedJS = panicOnError(minify.JS(rawJS)) //go:embed form_post.gohtml rawHTMLTemplate string @@ -44,7 +44,7 @@ var cspValue = strings.Join([]string{ `frame-ancestors 'none'`, }, "; ") -func mustMinify(s string, err error) string { +func panicOnError(s string, err error) string { if err != nil { panic(err) } diff --git a/internal/oidc/provider/formposthtml/formposthtml_test.go b/internal/oidc/provider/formposthtml/formposthtml_test.go index d5d69c9d..e28714c0 100644 --- a/internal/oidc/provider/formposthtml/formposthtml_test.go +++ b/internal/oidc/provider/formposthtml/formposthtml_test.go @@ -93,6 +93,6 @@ func TestContentSecurityPolicyHashes(t *testing.T) { } func TestHelpers(t *testing.T) { - require.Equal(t, "test", mustMinify("test", nil)) - require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) }) + require.Equal(t, "test", panicOnError("test", nil)) + require.PanicsWithError(t, "some error", func() { panicOnError("", fmt.Errorf("some error")) }) } diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index ffa33139..2833efa2 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -8,8 +8,6 @@ import ( "strings" "sync" - "go.pinniped.dev/internal/oidc/login" - corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "go.pinniped.dev/internal/oidc" @@ -20,6 +18,7 @@ import ( "go.pinniped.dev/internal/oidc/dynamiccodec" "go.pinniped.dev/internal/oidc/idpdiscovery" "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/oidc/login" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/token" "go.pinniped.dev/internal/plog" @@ -139,7 +138,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler( upstreamStateEncoder, csrfCookieEncoder, - login.NewGetHandler(), + login.NewGetHandler(incomingProvider.IssuerPath()+oidc.PinnipedLoginPath), login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage), ) diff --git a/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md b/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md index 7d411169..fc6b6ffd 100644 --- a/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md +++ b/site/content/docs/howto/configure-supervisor-with-workspace_one_access.md @@ -118,10 +118,6 @@ stringData: clientSecret: "" ``` -Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts -if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users. -For example, if you work at Acme Corp, choose something like `acme-corporate-workspace-one` over `my-idp`. - The following claims are returned by Workspace ONE Access. The `group` scope is required to use the `group_ids` and `group_names` claims. The `email` scope is required to use the `email` claim. The remaining claims are always available. From 0674215ef348d979e79655591560ed8dc8c771b2 Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Fri, 15 Apr 2022 22:43:53 -0400 Subject: [PATCH 59/77] Switch to go.uber.org/zap for JSON formatted logging Signed-off-by: Monis Khan --- cmd/pinniped-server/main.go | 13 +- cmd/pinniped-server/main_test.go | 7 +- cmd/pinniped/cmd/kubeconfig.go | 33 +- cmd/pinniped/cmd/kubeconfig_test.go | 2 +- cmd/pinniped/cmd/login_oidc.go | 19 +- cmd/pinniped/cmd/login_oidc_test.go | 40 +- cmd/pinniped/cmd/login_static.go | 10 +- cmd/pinniped/cmd/login_static_test.go | 25 +- cmd/pinniped/cmd/root.go | 15 +- cmd/pinniped/main.go | 6 +- deploy/concierge/deployment.yaml | 10 +- deploy/concierge/values.yaml | 4 + deploy/supervisor/helpers.lib.yaml | 8 +- deploy/supervisor/values.yaml | 4 + go.mod | 3 +- go.sum | 2 + internal/concierge/server/server.go | 22 +- internal/config/concierge/config.go | 6 +- internal/config/concierge/config_test.go | 186 ++++++++- internal/config/concierge/types.go | 4 +- internal/config/supervisor/config.go | 6 +- internal/config/supervisor/config_test.go | 123 +++++- internal/config/supervisor/types.go | 10 +- internal/controller/apicerts/certs_expirer.go | 11 +- .../controller/apicerts/certs_expirer_test.go | 13 + internal/controller/apicerts/certs_manager.go | 4 +- .../controller/apicerts/certs_observer.go | 6 +- .../conditionsutil/conditions_util.go.go | 4 +- .../impersonatorconfig/impersonator_config.go | 5 +- .../impersonator_config_test.go | 10 +- .../controller/kubecertagent/kubecertagent.go | 4 +- .../kubecertagent/kubecertagent_test.go | 50 ++- .../active_directory_upstream_watcher.go | 7 +- .../ldap_upstream_watcher.go | 6 +- .../oidc_upstream_watcher.go | 9 +- .../oidc_upstream_watcher_test.go | 6 +- internal/controllerlib/controller.go | 3 +- internal/controllerlib/option.go | 9 +- internal/controllerlib/recorder.go | 5 +- .../examplecontroller/controller/creating.go | 8 +- .../controllermanager/prepare_controllers.go | 14 +- internal/crypto/ptls/fips_strict.go | 7 +- internal/groupsuffix/groupsuffix.go | 11 +- internal/groupsuffix/groupsuffix_test.go | 4 +- internal/kubeclient/copied.go | 4 +- .../localuserauthenticator.go | 41 +- internal/plog/config.go | 107 +++++ internal/plog/config_test.go | 359 +++++++++++++++++ internal/plog/global.go | 77 ++++ internal/plog/klog.go | 32 -- internal/plog/level.go | 55 +-- internal/plog/level_test.go | 128 ------ internal/plog/plog.go | 189 ++++++--- internal/plog/plog_test.go | 366 ++++++++++++++++++ internal/plog/testing.go | 120 ++++++ internal/plog/zap.go | 187 +++++++++ .../registry/credentialrequest/rest_test.go | 4 +- internal/supervisor/server/server.go | 31 +- internal/testutil/testlogger/testlogger.go | 4 +- internal/testutil/transcript_logger.go | 1 + internal/upstreamoidc/upstreamoidc.go | 4 +- pkg/oidcclient/login.go | 29 +- pkg/oidcclient/login_test.go | 11 +- 63 files changed, 2033 insertions(+), 470 deletions(-) create mode 100644 internal/plog/config.go create mode 100644 internal/plog/config_test.go create mode 100644 internal/plog/global.go delete mode 100644 internal/plog/klog.go delete mode 100644 internal/plog/level_test.go create mode 100644 internal/plog/plog_test.go create mode 100644 internal/plog/testing.go create mode 100644 internal/plog/zap.go diff --git a/cmd/pinniped-server/main.go b/cmd/pinniped-server/main.go index fdce4f9d..b70aed8f 100644 --- a/cmd/pinniped-server/main.go +++ b/cmd/pinniped-server/main.go @@ -8,22 +8,23 @@ package main import ( + "fmt" "os" "path/filepath" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/klog/v2" - concierge "go.pinniped.dev/internal/concierge/server" // this side effect import ensures that we use fipsonly crypto in fips_strict mode. + concierge "go.pinniped.dev/internal/concierge/server" _ "go.pinniped.dev/internal/crypto/ptls" lua "go.pinniped.dev/internal/localuserauthenticator" + "go.pinniped.dev/internal/plog" supervisor "go.pinniped.dev/internal/supervisor/server" ) -//nolint: gochecknoglobals // these are swapped during unit tests. +// nolint: gochecknoglobals // these are swapped during unit tests. var ( - fail = klog.Fatalf + fail = plog.Fatal subcommands = map[string]func(){ "pinniped-concierge": concierge.Main, "pinniped-supervisor": supervisor.Main, @@ -33,11 +34,11 @@ var ( func main() { if len(os.Args) == 0 { - fail("missing os.Args") + fail(fmt.Errorf("missing os.Args")) } binary := filepath.Base(os.Args[0]) if subcommands[binary] == nil { - fail("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary) + fail(fmt.Errorf("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary)) } subcommands[binary]() } diff --git a/cmd/pinniped-server/main_test.go b/cmd/pinniped-server/main_test.go index 6a1e1e68..e262afe3 100644 --- a/cmd/pinniped-server/main_test.go +++ b/cmd/pinniped-server/main_test.go @@ -43,8 +43,11 @@ func TestEntrypoint(t *testing.T) { var logBuf bytes.Buffer testLog := log.New(&logBuf, "", 0) exited := "exiting via fatal" - fail = func(format string, v ...interface{}) { - testLog.Printf(format, v...) + fail = func(err error, keysAndValues ...interface{}) { + testLog.Print(err) + if len(keysAndValues) > 0 { + testLog.Print(keysAndValues...) + } panic(exited) } diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index e46eebde..1e59f481 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "net/http" "os" "strconv" @@ -19,8 +18,6 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-logr/logr" - "github.com/go-logr/stdr" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" @@ -34,23 +31,24 @@ import ( conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/net/phttp" + "go.pinniped.dev/internal/plog" ) type kubeconfigDeps struct { getPathToSelf func() (string, error) getClientset getConciergeClientsetFunc - log logr.Logger + log plog.MinLogger } func kubeconfigRealDeps() kubeconfigDeps { return kubeconfigDeps{ getPathToSelf: os.Executable, getClientset: getRealConciergeClientset, - log: stdr.New(log.New(os.Stderr, "", 0)), + log: plog.New(), } } -//nolint: gochecknoinits +// nolint: gochecknoinits func init() { getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps())) } @@ -175,6 +173,11 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f ctx, cancel := context.WithTimeout(ctx, flags.timeout) defer cancel() + // the log statements in this file assume that Info logs are unconditionally printed so we set the global level to info + if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelInfo, Format: plog.FormatCLI}); err != nil { + return err + } + // Validate api group suffix and immediately return an error if it is invalid. if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil { return fmt.Errorf("invalid API group suffix: %w", err) @@ -398,7 +401,7 @@ func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.I return credentialIssuer, nil } -func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log logr.Logger) error { +func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log plog.MinLogger) error { // Autodiscover the --concierge-mode. frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode) if err != nil { @@ -446,7 +449,7 @@ func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, return nil } -func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.Logger) { +func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log plog.MinLogger) { for _, strategy := range credentialIssuer.Status.Strategies { log.Info("found CredentialIssuer strategy", "type", strategy.Type, @@ -457,7 +460,7 @@ func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.L } } -func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log logr.Logger) error { +func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log plog.MinLogger) error { switch auth := authenticator.(type) { case *conciergev1alpha1.WebhookAuthenticator: // If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set @@ -556,7 +559,7 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E } } -func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) { +func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log plog.MinLogger) (*configv1alpha1.CredentialIssuer, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) defer cancelFunc() @@ -582,7 +585,7 @@ func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, return result, nil } -func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log logr.Logger) (metav1.Object, error) { +func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log plog.MinLogger) (metav1.Object, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) defer cancelFunc() @@ -643,7 +646,7 @@ func writeConfigAsYAML(out io.Writer, config clientcmdapi.Config) error { return nil } -func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log logr.Logger) error { +func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log plog.MinLogger) error { if flags.skipValidate { return nil } @@ -706,7 +709,7 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf log.Info("validated connection to the cluster", "attempts", attempts) return nil } - log.Error(err, "could not connect to cluster, retrying...", "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String()) + log.Info("could not connect to cluster, retrying...", "error", err, "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String()) } } } @@ -726,7 +729,7 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool return false } -func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams, log logr.Logger) error { +func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams, log plog.MinLogger) error { httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle) if err != nil { return err @@ -898,7 +901,7 @@ func selectUpstreamIDPNameAndType(pinnipedIDPs []idpdiscoveryv1alpha1.PinnipedID } } -func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string, log logr.Logger) (idpdiscoveryv1alpha1.IDPFlow, error) { +func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string, log plog.MinLogger) (idpdiscoveryv1alpha1.IDPFlow, error) { switch { case len(discoveredIDPFlows) == 0: // No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed. diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 76a216f0..9c3ee5e0 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -2889,7 +2889,7 @@ func TestGetKubeconfig(t *testing.T) { }) issuerEndpointPtr = &issuerEndpoint - testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements + testLog := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements cmd := kubeconfigCommand(kubeconfigDeps{ getPathToSelf: func() (string, error) { if tt.getPathToSelfErr != nil { diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index bf35a6ba..8f9378f5 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -20,7 +20,6 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" - "k8s.io/klog/v2/klogr" idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" "go.pinniped.dev/internal/execcredcache" @@ -33,7 +32,7 @@ import ( "go.pinniped.dev/pkg/oidcclient/oidctypes" ) -//nolint: gochecknoinits +// nolint: gochecknoinits func init() { loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps())) } @@ -125,7 +124,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { } func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen - pLogger, err := SetLogLevel(deps.lookupEnv) + pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv) if err != nil { plog.WarningErr("Received error while setting log level", err) } @@ -133,11 +132,11 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin // Initialize the session cache. var sessionOptions []filesession.Option - // If the hidden --debug-session-cache option is passed, log all the errors from the session cache with klog. + // If the hidden --debug-session-cache option is passed, log all the errors from the session cache. if flags.debugSessionCache { - logger := klogr.New().WithName("session") + logger := plog.WithName("session") sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) { - logger.Error(err, "error during session cache operation") + logger.Error("error during session cache operation", err) })) } sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...) @@ -145,7 +144,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin // Initialize the login handler. opts := []oidcclient.Option{ oidcclient.WithContext(cmd.Context()), - oidcclient.WithLogger(klogr.New()), + oidcclient.WithLogger(plog.Logr()), // nolint: staticcheck // old code with lots of log statements oidcclient.WithScopes(flags.scopes), oidcclient.WithSessionCache(sessionCache), } @@ -326,15 +325,15 @@ func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential { return &cred } -func SetLogLevel(lookupEnv func(string) (string, bool)) (plog.Logger, error) { +func SetLogLevel(ctx context.Context, lookupEnv func(string) (string, bool)) (plog.Logger, error) { debug, _ := lookupEnv("PINNIPED_DEBUG") if debug == "true" { - err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug) + err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug, Format: plog.FormatCLI}) if err != nil { return nil, err } } - logger := plog.New("Pinniped login: ") + logger := plog.New().WithName("pinniped-login") return logger, nil } diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index da0cfcb7..492891e2 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -10,18 +10,20 @@ import ( "fmt" "io/ioutil" "path/filepath" + "strings" "testing" "time" "github.com/stretchr/testify/require" + "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" - "k8s.io/klog/v2" + clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/oidcclient" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -38,6 +40,10 @@ func TestLoginOIDCCommand(t *testing.T) { time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC) + now, err := time.Parse(time.RFC3339Nano, "2028-10-11T23:37:26.953313745Z") + require.NoError(t, err) + nowStr := now.Local().Format(time.RFC1123) + tests := []struct { name string args []string @@ -342,8 +348,8 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 4, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", wantLogs: []string{ - "\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"", - "\"level\"=0 \"msg\"=\"Pinniped login: No concierge configured, skipping token credential exchange\"", + nowStr + ` pinniped-login cmd/login_oidc.go:222 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:242 No concierge configured, skipping token credential exchange`, }, }, { @@ -372,18 +378,20 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 11, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n", wantLogs: []string{ - "\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"", - "\"level\"=0 \"msg\"=\"Pinniped login: Exchanging token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1:1234/\"", - "\"level\"=0 \"msg\"=\"Pinniped login: Successfully exchanged token for cluster credential.\"", - "\"level\"=0 \"msg\"=\"Pinniped login: caching cluster credential for future use.\"", + nowStr + ` pinniped-login cmd/login_oidc.go:222 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:232 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:240 Successfully exchanged token for cluster credential.`, + nowStr + ` pinniped-login cmd/login_oidc.go:247 caching cluster credential for future use.`, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements - klog.SetLogger(testLogger.Logger) + var buf bytes.Buffer + fakeClock := clocktesting.NewFakeClock(now) + ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock))) + var ( gotOptions []oidcclient.Option ) @@ -428,7 +436,7 @@ func TestLoginOIDCCommand(t *testing.T) { cmd.SetOut(&stdout) cmd.SetErr(&stderr) cmd.SetArgs(tt.args) - err := cmd.Execute() + err = cmd.ExecuteContext(ctx) if tt.wantError { require.Error(t, err) } else { @@ -438,7 +446,15 @@ func TestLoginOIDCCommand(t *testing.T) { require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") require.Len(t, gotOptions, tt.wantOptionsCount) - require.Equal(t, tt.wantLogs, testLogger.Lines()) + require.Equal(t, tt.wantLogs, logLines(buf.String())) }) } } + +func logLines(logs string) []string { + if len(logs) == 0 { + return nil + } + + return strings.Split(strings.TrimSpace(logs), "\n") +} diff --git a/cmd/pinniped/cmd/login_static.go b/cmd/pinniped/cmd/login_static.go index 3642ffe1..2af80907 100644 --- a/cmd/pinniped/cmd/login_static.go +++ b/cmd/pinniped/cmd/login_static.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "fmt" - "io" "os" "path/filepath" "time" @@ -22,7 +21,7 @@ import ( "go.pinniped.dev/pkg/oidcclient/oidctypes" ) -//nolint: gochecknoinits +// nolint: gochecknoinits func init() { loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps())) } @@ -75,7 +74,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") - cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } + cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd, deps, flags) } mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore") mustMarkHidden(cmd, "concierge-namespace") @@ -83,8 +82,9 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command { return cmd } -func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error { - pLogger, err := SetLogLevel(deps.lookupEnv) +func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginParams) error { + out := cmd.OutOrStdout() + pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv) if err != nil { plog.WarningErr("Received error while setting log level", err) } diff --git a/cmd/pinniped/cmd/login_static_test.go b/cmd/pinniped/cmd/login_static_test.go index 5fd04e0d..7af4ac85 100644 --- a/cmd/pinniped/cmd/login_static_test.go +++ b/cmd/pinniped/cmd/login_static_test.go @@ -12,16 +12,15 @@ import ( "testing" "time" - "k8s.io/klog/v2" - - "go.pinniped.dev/internal/testutil/testlogger" - "github.com/stretchr/testify/require" + "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + clocktesting "k8s.io/utils/clock/testing" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/conciergeclient" ) @@ -35,6 +34,10 @@ func TestLoginStaticCommand(t *testing.T) { testCABundlePath := filepath.Join(tmpdir, "testca.pem") require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600)) + now, err := time.Parse(time.RFC3339Nano, "2038-12-07T23:37:26.953313745Z") + require.NoError(t, err) + nowStr := now.Local().Format(time.RFC1123) + tests := []struct { name string args []string @@ -136,7 +139,9 @@ func TestLoginStaticCommand(t *testing.T) { wantStderr: here.Doc(` Error: could not complete Concierge credential exchange: some concierge error `), - wantLogs: []string{"\"level\"=0 \"msg\"=\"Pinniped login: exchanging static token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1/\""}, + wantLogs: []string{ + nowStr + ` pinniped-login cmd/login_static.go:147 exchanging static token for cluster credential {"endpoint": "https://127.0.0.1/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, + }, }, { name: "invalid API group suffix", @@ -165,8 +170,10 @@ func TestLoginStaticCommand(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements - klog.SetLogger(testLogger.Logger) + var buf bytes.Buffer + fakeClock := clocktesting.NewFakeClock(now) + ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock))) + cmd := staticLoginCommand(staticLoginDeps{ lookupEnv: func(s string) (string, bool) { v, ok := tt.env[s] @@ -194,7 +201,7 @@ func TestLoginStaticCommand(t *testing.T) { cmd.SetOut(&stdout) cmd.SetErr(&stderr) cmd.SetArgs(tt.args) - err := cmd.Execute() + err := cmd.ExecuteContext(ctx) if tt.wantError { require.Error(t, err) } else { @@ -203,7 +210,7 @@ func TestLoginStaticCommand(t *testing.T) { require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") - require.Equal(t, tt.wantLogs, testLogger.Lines()) + require.Equal(t, tt.wantLogs, logLines(buf.String())) }) } } diff --git a/cmd/pinniped/cmd/root.go b/cmd/pinniped/cmd/root.go index 509e5a0b..4345b6c8 100644 --- a/cmd/pinniped/cmd/root.go +++ b/cmd/pinniped/cmd/root.go @@ -4,12 +4,14 @@ package cmd import ( - "os" + "context" "github.com/spf13/cobra" + + "go.pinniped.dev/internal/plog" ) -//nolint: gochecknoglobals +// nolint: gochecknoglobals var rootCmd = &cobra.Command{ Use: "pinniped", Short: "pinniped", @@ -19,8 +21,11 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - os.Exit(1) +func Execute() error { + defer plog.Setup()() + // the context does not matter here because it is unused when CLI formatting is provided + if err := plog.ValidateAndSetLogLevelAndFormatGlobally(context.Background(), plog.LogSpec{Format: plog.FormatCLI}); err != nil { + return err } + return rootCmd.Execute() } diff --git a/cmd/pinniped/main.go b/cmd/pinniped/main.go index d4776f24..93433b66 100644 --- a/cmd/pinniped/main.go +++ b/cmd/pinniped/main.go @@ -13,7 +13,7 @@ import ( _ "go.pinniped.dev/internal/crypto/ptls" ) -//nolint: gochecknoinits +// nolint: gochecknoinits func init() { // browsers like chrome like to write to our std out which breaks our JSON ExecCredential output // thus we redirect the browser's std out to our std err @@ -21,5 +21,7 @@ func init() { } func main() { - cmd.Execute() + if err := cmd.Execute(); err != nil { + os.Exit(1) + } } diff --git a/deploy/concierge/deployment.yaml b/deploy/concierge/deployment.yaml index 1cad1b6d..06bb8a1b 100644 --- a/deploy/concierge/deployment.yaml +++ b/deploy/concierge/deployment.yaml @@ -86,8 +86,14 @@ data: imagePullSecrets: - image-pull-secret (@ end @) - (@ if data.values.log_level: @) - logLevel: (@= getAndValidateLogLevel() @) + (@ if data.values.log_level or data.values.deprecated_log_format: @) + log: + (@ if data.values.log_level: @) + level: (@= getAndValidateLogLevel() @) + (@ end @) + (@ if data.values.deprecated_log_format: @) + format: (@= data.values.deprecated_log_format @) + (@ end @) (@ end @) --- #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": diff --git a/deploy/concierge/values.yaml b/deploy/concierge/values.yaml index d902f89f..9267b0fc 100644 --- a/deploy/concierge/values.yaml +++ b/deploy/concierge/values.yaml @@ -54,6 +54,10 @@ api_serving_certificate_renew_before_seconds: 2160000 #! Specify the verbosity of logging: info ("nice to know" information), debug (developer #! information), trace (timing information), all (kitchen sink). log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs. +#! Specify the format of logging: json (for machine parsable logs) and text (for legacy klog formatted logs). +#! By default, when this value is left unset, logs are formatted in json. +#! This configuration is deprecated and will be removed in a future release at which point logs will always be formatted as json. +deprecated_log_format: run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice diff --git a/deploy/supervisor/helpers.lib.yaml b/deploy/supervisor/helpers.lib.yaml index acd6d5a4..d759e874 100644 --- a/deploy/supervisor/helpers.lib.yaml +++ b/deploy/supervisor/helpers.lib.yaml @@ -54,8 +54,14 @@ _: #@ template.replace(data.values.custom_labels) #@ "labels": labels(), #@ "insecureAcceptExternalUnencryptedHttpRequests": data.values.deprecated_insecure_accept_external_unencrypted_http_requests #@ } +#@ if data.values.log_level or data.values.deprecated_log_format: +#@ config["log"] = {} +#@ end #@ if data.values.log_level: -#@ config["logLevel"] = getAndValidateLogLevel() +#@ config["log"]["level"] = getAndValidateLogLevel() +#@ end +#@ if data.values.deprecated_log_format: +#@ config["log"]["format"] = data.values.deprecated_log_format #@ end #@ if data.values.endpoints: #@ config["endpoints"] = data.values.endpoints diff --git a/deploy/supervisor/values.yaml b/deploy/supervisor/values.yaml index e0fd50f3..888d5038 100644 --- a/deploy/supervisor/values.yaml +++ b/deploy/supervisor/values.yaml @@ -57,6 +57,10 @@ service_loadbalancer_ip: #! e.g. 1.2.3.4 #! Specify the verbosity of logging: info ("nice to know" information), debug (developer information), trace (timing information), #! or all (kitchen sink). Do not use trace or all on production systems, as credentials may get logged. log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs. +#! Specify the format of logging: json (for machine parsable logs) and text (for legacy klog formatted logs). +#! By default, when this value is left unset, logs are formatted in json. +#! This configuration is deprecated and will be removed in a future release at which point logs will always be formatted as json. +deprecated_log_format: run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice diff --git a/go.mod b/go.mod index 251b43e2..5ee1267e 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/go-ldap/ldap/v3 v3.4.3 github.com/go-logr/logr v1.2.3 github.com/go-logr/stdr v1.2.2 + github.com/go-logr/zapr v1.2.3 github.com/gofrs/flock v0.8.1 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.8 @@ -64,6 +65,7 @@ require ( github.com/stretchr/testify v1.7.1 github.com/tdewolff/minify/v2 v2.11.2 go.uber.org/atomic v1.9.0 + go.uber.org/zap v1.21.0 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 @@ -168,7 +170,6 @@ require ( go.opentelemetry.io/otel/trace v1.6.3 // indirect go.opentelemetry.io/proto/otlp v0.15.0 // indirect go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.21.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect diff --git a/go.sum b/go.sum index b04534a9..86bc0ea4 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= diff --git a/internal/concierge/server/server.go b/internal/concierge/server/server.go index 1b724700..ebba1c4b 100644 --- a/internal/concierge/server/server.go +++ b/internal/concierge/server/server.go @@ -15,12 +15,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + apimachineryversion "k8s.io/apimachinery/pkg/version" genericapiserver "k8s.io/apiserver/pkg/server" genericoptions "k8s.io/apiserver/pkg/server/options" "k8s.io/client-go/pkg/version" "k8s.io/client-go/rest" - "k8s.io/component-base/logs" - "k8s.io/klog/v2" "go.pinniped.dev/internal/certauthority/dynamiccertauthority" "go.pinniped.dev/internal/concierge/apiserver" @@ -35,6 +34,7 @@ import ( "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/issuer" "go.pinniped.dev/internal/kubeclient" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/registry/credentialrequest" ) @@ -100,7 +100,7 @@ func addCommandlineFlagsToCommand(cmd *cobra.Command, app *App) { // Boot the aggregated API server, which will in turn boot the controllers. func (a *App) runServer(ctx context.Context) error { // Read the server config file. - cfg, err := concierge.FromPath(a.configPath) + cfg, err := concierge.FromPath(ctx, a.configPath) if err != nil { return fmt.Errorf("could not load config: %w", err) } @@ -250,16 +250,20 @@ func getAggregatedAPIServerConfig( return apiServerConfig, nil } -func main() error { // return an error instead of klog.Fatal to allow defer statements to run - logs.InitLogs() - defer logs.FlushLogs() +func main() error { // return an error instead of plog.Fatal to allow defer statements to run + defer plog.Setup()() // Dump out the time since compile (mostly useful for benchmarking our local development cycle latency). var timeSinceCompile time.Duration if buildDate, err := time.Parse(time.RFC3339, version.Get().BuildDate); err == nil { timeSinceCompile = time.Since(buildDate).Round(time.Second) } - klog.Infof("Running %s at %#v (%s since build)", rest.DefaultKubernetesUserAgent(), version.Get(), timeSinceCompile) + + plog.Always("Running concierge", + "user-agent", rest.DefaultKubernetesUserAgent(), + "version", versionInfo(version.Get()), + "time-since-build", timeSinceCompile, + ) ctx := genericapiserver.SetupSignalContext() @@ -268,6 +272,8 @@ func main() error { // return an error instead of klog.Fatal to allow defer stat func Main() { if err := main(); err != nil { - klog.Fatal(err) + plog.Fatal(err) } } + +type versionInfo apimachineryversion.Info // hide .String() method from plog diff --git a/internal/config/concierge/config.go b/internal/config/concierge/config.go index 4749c7ca..605f72d4 100644 --- a/internal/config/concierge/config.go +++ b/internal/config/concierge/config.go @@ -6,6 +6,7 @@ package concierge import ( + "context" "fmt" "io/ioutil" "strings" @@ -41,7 +42,7 @@ const ( // Note! The Config file should contain base64-encoded WebhookCABundle data. // This function will decode that base64-encoded data to PEM bytes to be stored // in the Config. -func FromPath(path string) (*Config, error) { +func FromPath(ctx context.Context, path string) (*Config, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) @@ -78,7 +79,8 @@ func FromPath(path string) (*Config, error) { return nil, fmt.Errorf("validate names: %w", err) } - if err := plog.ValidateAndSetLogLevelGlobally(config.LogLevel); err != nil { + plog.MaybeSetDeprecatedLogLevel(config.LogLevel, &config.Log) + if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, config.Log); err != nil { return nil, fmt.Errorf("validate log level: %w", err) } diff --git a/internal/config/concierge/config_test.go b/internal/config/concierge/config_test.go index 86139182..ce7c9929 100644 --- a/internal/config/concierge/config_test.go +++ b/internal/config/concierge/config_test.go @@ -4,6 +4,7 @@ package concierge import ( + "context" "io/ioutil" "os" "testing" @@ -90,9 +91,186 @@ func TestFromPath(t *testing.T) { Image: pointer.StringPtr("kube-cert-agent-image"), ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, }, - LogLevel: plog.LevelDebug, + LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelDebug), + Log: plog.LogSpec{ + Level: plog.LevelDebug, + }, }, }, + { + name: "Fully filled out new log struct", + yaml: here.Doc(` + --- + discovery: + url: https://some.discovery/url + api: + servingCertificate: + durationSeconds: 3600 + renewBeforeSeconds: 2400 + apiGroupSuffix: some.suffix.com + aggregatedAPIServerPort: 12345 + impersonationProxyServerPort: 4242 + names: + servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate + credentialIssuer: pinniped-config + apiService: pinniped-api + kubeCertAgentPrefix: kube-cert-agent-prefix + impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value + impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value + impersonationCACertificateSecret: impersonationCACertificateSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + agentServiceAccount: agentServiceAccount-value + extraName: extraName-value + labels: + myLabelKey1: myLabelValue1 + myLabelKey2: myLabelValue2 + kubeCertAgent: + namePrefix: kube-cert-agent-name-prefix- + image: kube-cert-agent-image + imagePullSecrets: [kube-cert-agent-image-pull-secret] + log: + level: all + format: json + `), + wantConfig: &Config{ + DiscoveryInfo: DiscoveryInfoSpec{ + URL: pointer.StringPtr("https://some.discovery/url"), + }, + APIConfig: APIConfigSpec{ + ServingCertificateConfig: ServingCertificateConfigSpec{ + DurationSeconds: pointer.Int64Ptr(3600), + RenewBeforeSeconds: pointer.Int64Ptr(2400), + }, + }, + APIGroupSuffix: pointer.StringPtr("some.suffix.com"), + AggregatedAPIServerPort: pointer.Int64Ptr(12345), + ImpersonationProxyServerPort: pointer.Int64Ptr(4242), + NamesConfig: NamesConfigSpec{ + ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", + CredentialIssuer: "pinniped-config", + APIService: "pinniped-api", + ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", + ImpersonationClusterIPService: "impersonationClusterIPService-value", + ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", + ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", + ImpersonationSignerSecret: "impersonationSignerSecret-value", + AgentServiceAccount: "agentServiceAccount-value", + }, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + KubeCertAgentConfig: KubeCertAgentSpec{ + NamePrefix: pointer.StringPtr("kube-cert-agent-name-prefix-"), + Image: pointer.StringPtr("kube-cert-agent-image"), + ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, + }, + Log: plog.LogSpec{ + Level: plog.LevelAll, + Format: plog.FormatJSON, + }, + }, + }, + { + name: "Fully filled out old log and new log struct", + yaml: here.Doc(` + --- + discovery: + url: https://some.discovery/url + api: + servingCertificate: + durationSeconds: 3600 + renewBeforeSeconds: 2400 + apiGroupSuffix: some.suffix.com + aggregatedAPIServerPort: 12345 + impersonationProxyServerPort: 4242 + names: + servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate + credentialIssuer: pinniped-config + apiService: pinniped-api + kubeCertAgentPrefix: kube-cert-agent-prefix + impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value + impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value + impersonationCACertificateSecret: impersonationCACertificateSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + agentServiceAccount: agentServiceAccount-value + extraName: extraName-value + labels: + myLabelKey1: myLabelValue1 + myLabelKey2: myLabelValue2 + kubeCertAgent: + namePrefix: kube-cert-agent-name-prefix- + image: kube-cert-agent-image + imagePullSecrets: [kube-cert-agent-image-pull-secret] + logLevel: debug + log: + level: all + format: json + `), + wantConfig: &Config{ + DiscoveryInfo: DiscoveryInfoSpec{ + URL: pointer.StringPtr("https://some.discovery/url"), + }, + APIConfig: APIConfigSpec{ + ServingCertificateConfig: ServingCertificateConfigSpec{ + DurationSeconds: pointer.Int64Ptr(3600), + RenewBeforeSeconds: pointer.Int64Ptr(2400), + }, + }, + APIGroupSuffix: pointer.StringPtr("some.suffix.com"), + AggregatedAPIServerPort: pointer.Int64Ptr(12345), + ImpersonationProxyServerPort: pointer.Int64Ptr(4242), + NamesConfig: NamesConfigSpec{ + ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", + CredentialIssuer: "pinniped-config", + APIService: "pinniped-api", + ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", + ImpersonationClusterIPService: "impersonationClusterIPService-value", + ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", + ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", + ImpersonationSignerSecret: "impersonationSignerSecret-value", + AgentServiceAccount: "agentServiceAccount-value", + }, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + KubeCertAgentConfig: KubeCertAgentSpec{ + NamePrefix: pointer.StringPtr("kube-cert-agent-name-prefix-"), + Image: pointer.StringPtr("kube-cert-agent-image"), + ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, + }, + LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelDebug), + Log: plog.LogSpec{ + Level: plog.LevelDebug, + Format: plog.FormatJSON, + }, + }, + }, + { + name: "invalid log format", + yaml: here.Doc(` + --- + names: + servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate + credentialIssuer: pinniped-config + apiService: pinniped-api + impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value + impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value + impersonationCACertificateSecret: impersonationCACertificateSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + agentServiceAccount: agentServiceAccount-value + log: + level: all + format: snorlax + `), + wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: invalid log format, valid choices are the empty string, json and text", + }, { name: "When only the required fields are present, causes other fields to be defaulted", yaml: here.Doc(` @@ -404,6 +582,8 @@ func TestFromPath(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { + // this is a serial test because it sets the global logger + // Write yaml to temp file f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*") require.NoError(t, err) @@ -417,7 +597,9 @@ func TestFromPath(t *testing.T) { require.NoError(t, err) // Test FromPath() - config, err := FromPath(f.Name()) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + config, err := FromPath(ctx, f.Name()) if test.wantError != "" { require.EqualError(t, err, test.wantError) diff --git a/internal/config/concierge/types.go b/internal/config/concierge/types.go index 1717aa3c..f444dc82 100644 --- a/internal/config/concierge/types.go +++ b/internal/config/concierge/types.go @@ -15,7 +15,9 @@ type Config struct { NamesConfig NamesConfigSpec `json:"names"` KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"` Labels map[string]string `json:"labels"` - LogLevel plog.LogLevel `json:"logLevel"` + // Deprecated: use log.level instead + LogLevel *plog.LogLevel `json:"logLevel"` + Log plog.LogSpec `json:"log"` } // DiscoveryInfoSpec contains configuration knobs specific to diff --git a/internal/config/supervisor/config.go b/internal/config/supervisor/config.go index b7baadf8..192d9790 100644 --- a/internal/config/supervisor/config.go +++ b/internal/config/supervisor/config.go @@ -6,6 +6,7 @@ package supervisor import ( + "context" "fmt" "io/ioutil" "net" @@ -28,7 +29,7 @@ const ( // FromPath loads an Config from a provided local file path, inserts any // defaults (from the Config documentation), and verifies that the config is // valid (Config documentation). -func FromPath(path string) (*Config, error) { +func FromPath(ctx context.Context, path string) (*Config, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, fmt.Errorf("read file: %w", err) @@ -53,7 +54,8 @@ func FromPath(path string) (*Config, error) { return nil, fmt.Errorf("validate names: %w", err) } - if err := plog.ValidateAndSetLogLevelGlobally(config.LogLevel); err != nil { + plog.MaybeSetDeprecatedLogLevel(config.LogLevel, &config.Log) + if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, config.Log); err != nil { return nil, fmt.Errorf("validate log level: %w", err) } diff --git a/internal/config/supervisor/config_test.go b/internal/config/supervisor/config_test.go index 5d4d7a85..ac4651a7 100644 --- a/internal/config/supervisor/config_test.go +++ b/internal/config/supervisor/config_test.go @@ -4,6 +4,7 @@ package supervisor import ( + "context" "fmt" "io/ioutil" "os" @@ -13,6 +14,7 @@ import ( "k8s.io/utils/pointer" "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/plog" ) func TestFromPath(t *testing.T) { @@ -40,6 +42,7 @@ func TestFromPath(t *testing.T) { network: tcp address: 127.0.0.1:1234 insecureAcceptExternalUnencryptedHttpRequests: false + logLevel: trace `), wantConfig: &Config{ APIGroupSuffix: pointer.StringPtr("some.suffix.com"), @@ -61,8 +64,122 @@ func TestFromPath(t *testing.T) { }, }, AllowExternalHTTP: false, + LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelTrace), + Log: plog.LogSpec{ + Level: plog.LevelTrace, + }, }, }, + { + name: "Happy with new log field", + yaml: here.Doc(` + --- + apiGroupSuffix: some.suffix.com + labels: + myLabelKey1: myLabelValue1 + myLabelKey2: myLabelValue2 + names: + defaultTLSCertificateSecret: my-secret-name + endpoints: + https: + network: unix + address: :1234 + http: + network: tcp + address: 127.0.0.1:1234 + insecureAcceptExternalUnencryptedHttpRequests: false + log: + level: info + format: text + `), + wantConfig: &Config{ + APIGroupSuffix: pointer.StringPtr("some.suffix.com"), + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + NamesConfig: NamesConfigSpec{ + DefaultTLSCertificateSecret: "my-secret-name", + }, + Endpoints: &Endpoints{ + HTTPS: &Endpoint{ + Network: "unix", + Address: ":1234", + }, + HTTP: &Endpoint{ + Network: "tcp", + Address: "127.0.0.1:1234", + }, + }, + AllowExternalHTTP: false, + Log: plog.LogSpec{ + Level: plog.LevelInfo, + Format: plog.FormatText, + }, + }, + }, + { + name: "Happy with old and new log field", + yaml: here.Doc(` + --- + apiGroupSuffix: some.suffix.com + labels: + myLabelKey1: myLabelValue1 + myLabelKey2: myLabelValue2 + names: + defaultTLSCertificateSecret: my-secret-name + endpoints: + https: + network: unix + address: :1234 + http: + network: tcp + address: 127.0.0.1:1234 + insecureAcceptExternalUnencryptedHttpRequests: false + logLevel: trace + log: + level: info + format: text + `), + wantConfig: &Config{ + APIGroupSuffix: pointer.StringPtr("some.suffix.com"), + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + NamesConfig: NamesConfigSpec{ + DefaultTLSCertificateSecret: "my-secret-name", + }, + Endpoints: &Endpoints{ + HTTPS: &Endpoint{ + Network: "unix", + Address: ":1234", + }, + HTTP: &Endpoint{ + Network: "tcp", + Address: "127.0.0.1:1234", + }, + }, + AllowExternalHTTP: false, + LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelTrace), + Log: plog.LogSpec{ + Level: plog.LevelTrace, + Format: plog.FormatText, + }, + }, + }, + { + name: "bad log format", + yaml: here.Doc(` + --- + names: + defaultTLSCertificateSecret: my-secret-name + log: + level: info + format: cli + `), + wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: invalid log format, valid choices are the empty string, json and text", + }, { name: "When only the required fields are present, causes other fields to be defaulted", yaml: here.Doc(` @@ -307,7 +424,7 @@ func TestFromPath(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - t.Parallel() + // this is a serial test because it sets the global logger // Write yaml to temp file f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*") @@ -322,7 +439,9 @@ func TestFromPath(t *testing.T) { require.NoError(t, err) // Test FromPath() - config, err := FromPath(f.Name()) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + config, err := FromPath(ctx, f.Name()) if test.wantError != "" { require.EqualError(t, err, test.wantError) diff --git a/internal/config/supervisor/types.go b/internal/config/supervisor/types.go index e61d4068..147845fb 100644 --- a/internal/config/supervisor/types.go +++ b/internal/config/supervisor/types.go @@ -11,10 +11,12 @@ import ( // Config contains knobs to setup an instance of the Pinniped Supervisor. type Config struct { - APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"` - Labels map[string]string `json:"labels"` - NamesConfig NamesConfigSpec `json:"names"` - LogLevel plog.LogLevel `json:"logLevel"` + APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"` + Labels map[string]string `json:"labels"` + NamesConfig NamesConfigSpec `json:"names"` + // Deprecated: use log.level instead + LogLevel *plog.LogLevel `json:"logLevel"` + Log plog.LogSpec `json:"log"` Endpoints *Endpoints `json:"endpoints"` AllowExternalHTTP stringOrBoolAsBool `json:"insecureAcceptExternalUnencryptedHttpRequests"` } diff --git a/internal/controller/apicerts/certs_expirer.go b/internal/controller/apicerts/certs_expirer.go index dbd48628..bb4a28f8 100644 --- a/internal/controller/apicerts/certs_expirer.go +++ b/internal/controller/apicerts/certs_expirer.go @@ -32,6 +32,8 @@ type certsExpirerController struct { renewBefore time.Duration secretKey string + + logger plog.Logger } // NewCertsExpirerController returns a controllerlib.Controller that will delete a @@ -45,10 +47,12 @@ func NewCertsExpirerController( withInformer pinnipedcontroller.WithInformerOptionFunc, renewBefore time.Duration, secretKey string, + logger plog.Logger, ) controllerlib.Controller { + const name = "certs-expirer-controller" return controllerlib.New( controllerlib.Config{ - Name: "certs-expirer-controller", + Name: name, Syncer: &certsExpirerController{ namespace: namespace, certsSecretResourceName: certsSecretResourceName, @@ -56,6 +60,7 @@ func NewCertsExpirerController( secretInformer: secretInformer, renewBefore: renewBefore, secretKey: secretKey, + logger: logger.WithName(name), }, }, withInformer( @@ -74,7 +79,7 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err) } if notFound { - plog.Info("secret does not exist yet or was deleted", + c.logger.Info("secret does not exist yet or was deleted", "controller", ctx.Name, "namespace", c.namespace, "name", c.certsSecretResourceName, @@ -91,7 +96,7 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error { certAge := time.Since(notBefore) renewDelta := certAge - c.renewBefore - plog.Debug("found renew delta", + c.logger.Debug("found renew delta", "controller", ctx.Name, "namespace", c.namespace, "name", c.certsSecretResourceName, diff --git a/internal/controller/apicerts/certs_expirer_test.go b/internal/controller/apicerts/certs_expirer_test.go index 9741de5d..3e33ee6a 100644 --- a/internal/controller/apicerts/certs_expirer_test.go +++ b/internal/controller/apicerts/certs_expirer_test.go @@ -4,12 +4,15 @@ package apicerts import ( + "bytes" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "errors" + "io" + "strings" "testing" "time" @@ -24,6 +27,7 @@ import ( kubetesting "k8s.io/client-go/testing" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" ) @@ -101,6 +105,7 @@ func TestExpirerControllerFilters(t *testing.T) { withInformer.WithInformer, 0, // renewBefore, not needed "", // not needed + plog.TestLogger(t, io.Discard), ) unrelated := corev1.Secret{} @@ -125,10 +130,12 @@ func TestExpirerControllerSync(t *testing.T) { fillSecretData func(*testing.T, map[string][]byte) configKubeAPIClient func(*kubernetesfake.Clientset) wantDelete bool + wantLog string wantError string }{ { name: "secret does not exist", + wantLog: `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"certs-expirer-controller","caller":"apicerts/certs_expirer.go:$apicerts.(*certsExpirerController).Sync","message":"secret does not exist yet or was deleted","controller":"","namespace":"some-namespace","name":"some-resource-name","key":"some-awesome-key","renewBefore":"0s"}`, wantDelete: false, }, { @@ -251,6 +258,8 @@ func TestExpirerControllerSync(t *testing.T) { 0, ) + var log bytes.Buffer + c := NewCertsExpirerController( namespace, certsSecretResourceName, @@ -259,6 +268,7 @@ func TestExpirerControllerSync(t *testing.T) { controllerlib.WithInformer, test.renewBefore, fakeTestKey, + plog.TestLogger(t, &log), ) // Must start informers before calling TestRunSynchronously(). @@ -268,6 +278,9 @@ func TestExpirerControllerSync(t *testing.T) { err := controllerlib.TestSync(t, c, controllerlib.Context{ Context: ctx, }) + if len(test.wantLog) > 0 { + require.Equal(t, test.wantLog, strings.TrimSpace(log.String())) + } if test.wantError != "" { require.EqualError(t, err, test.wantError) return diff --git a/internal/controller/apicerts/certs_manager.go b/internal/controller/apicerts/certs_manager.go index c3d8bf36..d8551452 100644 --- a/internal/controller/apicerts/certs_manager.go +++ b/internal/controller/apicerts/certs_manager.go @@ -12,11 +12,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2" "go.pinniped.dev/internal/certauthority" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/plog" ) const ( @@ -139,6 +139,6 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error { return fmt.Errorf("could not create secret: %w", err) } - klog.Info("certsManagerController Sync successfully created secret") + plog.Info("certsManagerController Sync successfully created secret") return nil } diff --git a/internal/controller/apicerts/certs_observer.go b/internal/controller/apicerts/certs_observer.go index f01312d5..704020f7 100644 --- a/internal/controller/apicerts/certs_observer.go +++ b/internal/controller/apicerts/certs_observer.go @@ -8,11 +8,11 @@ import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" corev1informers "k8s.io/client-go/informers/core/v1" - "k8s.io/klog/v2" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" + "go.pinniped.dev/internal/plog" ) type certsObserverController struct { @@ -55,7 +55,7 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error { return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err) } if notFound { - klog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted") + plog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted") // The secret does not exist yet or was deleted. c.dynamicCertProvider.UnsetCertKeyContent() return nil @@ -66,6 +66,6 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error { return fmt.Errorf("failed to set serving cert/key content from secret %s/%s: %w", c.namespace, c.certsSecretResourceName, err) } - klog.Info("certsObserverController Sync updated certs in the dynamic cert provider") + plog.Info("certsObserverController Sync updated certs in the dynamic cert provider") return nil } diff --git a/internal/controller/conditionsutil/conditions_util.go.go b/internal/controller/conditionsutil/conditions_util.go.go index 67f13b03..dec4695a 100644 --- a/internal/controller/conditionsutil/conditions_util.go.go +++ b/internal/controller/conditionsutil/conditions_util.go.go @@ -6,15 +6,15 @@ package conditionsutil import ( "sort" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/equality" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/plog" ) // Merge merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. -func Merge(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log logr.Logger) bool { +func Merge(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log plog.MinLogger) bool { hadErrorCondition := false for i := range conditions { cond := conditions[i].DeepCopy() diff --git a/internal/controller/impersonatorconfig/impersonator_config.go b/internal/controller/impersonatorconfig/impersonator_config.go index 4cd7b0b0..aa261e42 100644 --- a/internal/controller/impersonatorconfig/impersonator_config.go +++ b/internal/controller/impersonatorconfig/impersonator_config.go @@ -45,6 +45,7 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/plog" ) const ( @@ -132,8 +133,8 @@ func NewImpersonatorConfigController( impersonationSigningCertProvider: impersonationSigningCertProvider, impersonatorFunc: impersonatorFunc, tlsServingCertDynamicCertProvider: dynamiccert.NewServingCert("impersonation-proxy-serving-cert"), - infoLog: log.V(2), - debugLog: log.V(4), + infoLog: log.V(plog.KlogLevelInfo), + debugLog: log.V(plog.KlogLevelDebug), }, }, withInformer(credentialIssuerInformer, diff --git a/internal/controller/impersonatorconfig/impersonator_config_test.go b/internal/controller/impersonatorconfig/impersonator_config_test.go index c080b575..6b760d3d 100644 --- a/internal/controller/impersonatorconfig/impersonator_config_test.go +++ b/internal/controller/impersonatorconfig/impersonator_config_test.go @@ -43,8 +43,8 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/kubeclient" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/internal/testutil/testlogger" ) func TestImpersonatorConfigControllerOptions(t *testing.T) { @@ -63,7 +63,6 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) { var credIssuerInformerFilter controllerlib.Filter var servicesInformerFilter controllerlib.Filter var secretsInformerFilter controllerlib.Filter - var testLog *testlogger.Logger it.Before(func() { r = require.New(t) @@ -73,7 +72,6 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) { credIssuerInformer := pinnipedInformerFactory.Config().V1alpha1().CredentialIssuers() servicesInformer := sharedInformerFactory.Core().V1().Services() secretsInformer := sharedInformerFactory.Core().V1().Secrets() - testLog = testlogger.New(t) _ = NewImpersonatorConfigController( installedInNamespace, @@ -94,7 +92,7 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) { nil, caSignerName, nil, - testLog.Logger, + plog.Logr(), // nolint: staticcheck // old test with no log assertions ) credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer) servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer) @@ -292,7 +290,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { var testHTTPServerInterruptCh chan struct{} var queue *testQueue var validClientCert *tls.Certificate - var testLog *testlogger.Logger var impersonatorFunc = func( port int, @@ -563,7 +560,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { impersonatorFunc, caSignerName, signingCertProvider, - testLog.Logger, + plog.Logr(), // nolint: staticcheck // old test with no log assertions ) controllerlib.TestWrap(t, subject, func(syncer controllerlib.Syncer) controllerlib.Syncer { tlsServingCertDynamicCertProvider = syncer.(*impersonatorConfigController).tlsServingCertDynamicCertProvider @@ -1120,7 +1117,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { signingCASecret = newSigningKeySecret(caSignerName, signingCACertPEM, signingCAKeyPEM) validClientCert, err = ca.IssueClientCert("username", nil, time.Hour) r.NoError(err) - testLog = testlogger.New(t) }) it.After(func() { diff --git a/internal/controller/kubecertagent/kubecertagent.go b/internal/controller/kubecertagent/kubecertagent.go index 230cf0ba..c19b34b1 100644 --- a/internal/controller/kubecertagent/kubecertagent.go +++ b/internal/controller/kubecertagent/kubecertagent.go @@ -28,7 +28,6 @@ import ( corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" - "k8s.io/klog/v2/klogr" "k8s.io/utils/clock" "k8s.io/utils/pointer" @@ -39,6 +38,7 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/kubeclient" + "go.pinniped.dev/internal/plog" ) const ( @@ -179,7 +179,7 @@ func NewAgentController( dynamicCertProvider, &clock.RealClock{}, cache.NewExpiring(), - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements ) } diff --git a/internal/controller/kubecertagent/kubecertagent_test.go b/internal/controller/kubecertagent/kubecertagent_test.go index 38736213..3e8016b8 100644 --- a/internal/controller/kubecertagent/kubecertagent_test.go +++ b/internal/controller/kubecertagent/kubecertagent_test.go @@ -4,8 +4,10 @@ package kubecertagent import ( + "bytes" "context" "fmt" + "strings" "testing" "time" @@ -35,8 +37,8 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/kubeclient" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/test/testlib" ) @@ -339,7 +341,7 @@ func TestAgentController(t *testing.T) { "could not ensure agent deployment: some creation error", }, 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"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantStrategy: &configv1alpha1.CredentialIssuerStrategy{ Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, @@ -386,7 +388,7 @@ func TestAgentController(t *testing.T) { `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"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantAgentDeployment: healthyAgentDeployment, wantDeploymentActionVerbs: []string{"list", "watch", "create"}, @@ -435,7 +437,7 @@ func TestAgentController(t *testing.T) { `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"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantAgentDeployment: healthyAgentDeploymentWithDefaultedPaths, wantDeploymentActionVerbs: []string{"list", "watch", "create"}, @@ -461,8 +463,8 @@ func TestAgentController(t *testing.T) { "could not find a healthy agent pod (1 candidate)", }, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, - `kube-cert-agent-controller "level"=0 "msg"="creating new deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantAgentDeployment: healthyAgentDeployment, wantDeploymentActionVerbs: []string{"list", "watch", "delete", "create"}, // must recreate deployment when Selector field changes @@ -496,7 +498,7 @@ func TestAgentController(t *testing.T) { "could not ensure agent deployment: some delete error", }, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantAgentDeployment: healthyAgentDeploymentWithOldStyleSelector, // couldn't be deleted, so it didn't change // delete to try to recreate deployment when Selector field changes, but delete always fails, so keeps trying to delete @@ -532,9 +534,9 @@ func TestAgentController(t *testing.T) { "could not ensure agent deployment: some create error", }, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, - `kube-cert-agent-controller "level"=0 "msg"="creating new deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, - `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"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantAgentDeployment: nil, // was deleted, but couldn't be recreated // delete to try to recreate deployment when Selector field changes, but create always fails, so keeps trying to recreate @@ -584,7 +586,7 @@ func TestAgentController(t *testing.T) { "could not find a healthy agent pod (1 candidate)", }, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="updating existing deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"updating existing deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, wantAgentDeployment: healthyAgentDeploymentWithExtraLabels, wantDeploymentActionVerbs: []string{"list", "watch", "update"}, @@ -619,7 +621,7 @@ func TestAgentController(t *testing.T) { LastUpdateTime: metav1.NewTime(now), }, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="updating existing deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"updating existing deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, }, }, { @@ -931,8 +933,8 @@ func TestAgentController(t *testing.T) { // delete to try to recreate deployment when Selector field changes, but delete always fails, so keeps trying to delete wantDeploymentActionVerbs: []string{"list", "watch", "delete", "delete"}, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, - `kube-cert-agent-controller "level"=0 "msg"="successfully loaded signing key from agent pod into cache"`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`, }, wantDeploymentDeleteActionOpts: []metav1.DeleteOptions{ testutil.NewPreconditions(healthyAgentDeploymentWithOldStyleSelector.UID, healthyAgentDeploymentWithOldStyleSelector.ResourceVersion), @@ -962,7 +964,7 @@ func TestAgentController(t *testing.T) { wantAgentDeployment: healthyAgentDeployment, wantDeploymentActionVerbs: []string{"list", "watch"}, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="successfully loaded signing key from agent pod into cache"`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`, }, wantStrategy: &configv1alpha1.CredentialIssuerStrategy{ Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, @@ -996,7 +998,7 @@ func TestAgentController(t *testing.T) { wantAgentDeployment: healthyAgentDeployment, wantDeploymentActionVerbs: []string{"list", "watch"}, wantDistinctLogs: []string{ - `kube-cert-agent-controller "level"=0 "msg"="successfully loaded signing key from agent pod into cache"`, + `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`, }, wantStrategy: &configv1alpha1.CredentialIssuerStrategy{ Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, @@ -1028,7 +1030,9 @@ func TestAgentController(t *testing.T) { } kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0) - log := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements + + var buf bytes.Buffer + log := plog.TestZapr(t, &buf) ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1066,7 +1070,7 @@ func TestAgentController(t *testing.T) { mockDynamicCert, fakeClock, execCache, - log.Logger, + log, ) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -1081,7 +1085,7 @@ func TestAgentController(t *testing.T) { allAllowedErrors = append(allAllowedErrors, tt.alsoAllowUndesiredDistinctErrors...) assert.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.Equal(t, tt.wantDistinctLogs, deduplicate(logLines(buf.String())), "unexpected logs") // Assert on all actions that happened to deployments. var actualDeploymentActionVerbs []string @@ -1124,6 +1128,14 @@ func TestAgentController(t *testing.T) { } } +func logLines(logs string) []string { + if len(logs) == 0 { + return nil + } + + return strings.Split(strings.TrimSpace(logs), "\n") +} + func TestMergeLabelsAndAnnotations(t *testing.T) { t.Parallel() diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 4fb699b7..4aaa41b9 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -13,12 +13,10 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/google/uuid" - "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" corev1informers "k8s.io/client-go/informers/core/v1" - "k8s.io/klog/v2/klogr" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" @@ -28,6 +26,7 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) @@ -360,7 +359,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, } func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, conditions []*v1alpha1.Condition) { - log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) + log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) @@ -379,7 +378,7 @@ func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, ups ActiveDirectoryIdentityProviders(upstream.Namespace). UpdateStatus(ctx, updated, metav1.UpdateOptions{}) if err != nil { - log.Error(err, "failed to update status") + log.Error("failed to update status", err) } } diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index fd57cc28..a942bbf9 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -12,7 +12,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" corev1informers "k8s.io/client-go/informers/core/v1" - "k8s.io/klog/v2/klogr" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" @@ -22,6 +21,7 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) @@ -252,7 +252,7 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * } func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) { - log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) + log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) @@ -271,6 +271,6 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al LDAPIdentityProviders(upstream.Namespace). UpdateStatus(ctx, updated, metav1.UpdateOptions{}) if err != nil { - log.Error(err, "failed to update status") + log.Error("failed to update status", err) } } diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 56f34776..2faff38c 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -14,8 +14,6 @@ import ( "strings" "time" - "k8s.io/apimachinery/pkg/util/sets" - "github.com/coreos/go-oidc/v3/oidc" "github.com/go-logr/logr" "golang.org/x/oauth2" @@ -24,6 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/cache" + "k8s.io/apimachinery/pkg/util/sets" corev1informers "k8s.io/client-go/informers/core/v1" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -36,6 +35,7 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamoidc" ) @@ -67,7 +67,7 @@ const ( ) var ( - disallowedAdditionalAuthorizeParameters = map[string]bool{ //nolint: gochecknoglobals + disallowedAdditionalAuthorizeParameters = map[string]bool{ // nolint: gochecknoglobals // Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters // that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise // happily treat the user's config as an override. Users can already set the "client_id" and "scope" params @@ -331,8 +331,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1 discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer) if err != nil { - const klogLevelTrace = 6 - c.log.V(klogLevelTrace).WithValues( + c.log.V(plog.KlogLevelTrace).WithValues( "namespace", upstream.Namespace, "name", upstream.Name, "issuer", upstream.Spec.Issuer, diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 62e308e2..bbaeb2d4 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -29,6 +29,7 @@ import ( "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/internal/testutil/testlogger" @@ -78,7 +79,6 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) fakeKubeClient := fake.NewSimpleClientset() kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) - testLog := testlogger.New(t) cache := provider.NewDynamicUpstreamIDPProvider() cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, @@ -91,7 +91,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { nil, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), secretInformer, - testLog.Logger, + plog.Logr(), // nolint: staticcheck // old test with no log assertions withInformer.WithInformer, ) @@ -1400,7 +1400,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) - testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements + testLog := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements cache := provider.NewDynamicUpstreamIDPProvider() cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, diff --git a/internal/controllerlib/controller.go b/internal/controllerlib/controller.go index 4d3940a7..9314cd47 100644 --- a/internal/controllerlib/controller.go +++ b/internal/controllerlib/controller.go @@ -15,7 +15,6 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/events" "k8s.io/client-go/util/workqueue" - "k8s.io/klog/v2" "go.pinniped.dev/internal/plog" ) @@ -215,7 +214,7 @@ func (c *controller) handleKey(key Key, err error) { if errors.Is(err, ErrSyntheticRequeue) { // logging this helps detecting wedged controllers with missing pre-requirements - klog.V(4).InfoS("requested synthetic requeue", "controller", c.Name(), "key", key) + plog.Debug("requested synthetic requeue", "controller", c.Name(), "key", key) } else { utilruntime.HandleError(fmt.Errorf("%s: %v failed with: %w", c.Name(), key, err)) } diff --git a/internal/controllerlib/option.go b/internal/controllerlib/option.go index 38e89f58..9b3b4510 100644 --- a/internal/controllerlib/option.go +++ b/internal/controllerlib/option.go @@ -13,7 +13,8 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/events" "k8s.io/client-go/util/workqueue" - "k8s.io/klog/v2" + + "go.pinniped.dev/internal/plog" ) type Option func(*controller) @@ -62,7 +63,7 @@ func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Opti AddFunc: func(obj interface{}) { object := metaOrDie(obj) if filter.Add(object) { - klog.V(4).InfoS("handling add", + plog.Debug("handling add", "controller", c.Name(), "namespace", object.GetNamespace(), "name", object.GetName(), @@ -76,7 +77,7 @@ func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Opti oldObject := metaOrDie(oldObj) newObject := metaOrDie(newObj) if filter.Update(oldObject, newObject) { - klog.V(4).InfoS("handling update", + plog.Debug("handling update", "controller", c.Name(), "namespace", newObject.GetNamespace(), "name", newObject.GetName(), @@ -101,7 +102,7 @@ func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Opti } } if filter.Delete(accessor) { - klog.V(4).InfoS("handling delete", + plog.Debug("handling delete", "controller", c.Name(), "namespace", accessor.GetNamespace(), "name", accessor.GetName(), diff --git a/internal/controllerlib/recorder.go b/internal/controllerlib/recorder.go index d25d00e3..b329c5b6 100644 --- a/internal/controllerlib/recorder.go +++ b/internal/controllerlib/recorder.go @@ -8,7 +8,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/events" - "k8s.io/klog/v2" + + "go.pinniped.dev/internal/plog" ) var _ events.EventRecorder = klogRecorder{} @@ -16,7 +17,7 @@ var _ events.EventRecorder = klogRecorder{} type klogRecorder struct{} func (n klogRecorder) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { - klog.V(4).InfoS("recording event", + plog.Debug("recording event", "regarding", regarding, "related", related, "eventtype", eventtype, diff --git a/internal/controllerlib/test/integration/examplecontroller/controller/creating.go b/internal/controllerlib/test/integration/examplecontroller/controller/creating.go index 17fd15e2..d24f4044 100644 --- a/internal/controllerlib/test/integration/examplecontroller/controller/creating.go +++ b/internal/controllerlib/test/integration/examplecontroller/controller/creating.go @@ -14,10 +14,10 @@ import ( corev1informers "k8s.io/client-go/informers/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/events" - "k8s.io/klog/v2" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api" + "go.pinniped.dev/internal/plog" ) //nolint:funlen @@ -59,7 +59,7 @@ func NewExampleCreatingController( } generateSecret := func(service *corev1.Service) error { - klog.V(4).InfoS("generating new secret for service", "namespace", service.Namespace, "name", service.Name) + plog.Debug("generating new secret for service", "namespace", service.Namespace, "name", service.Name) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -97,7 +97,7 @@ func NewExampleCreatingController( return nil // drop from queue because we cannot safely update this secret } - klog.V(4).InfoS("updating data in existing secret", "namespace", secret.Namespace, "name", secret.Name) + plog.Debug("updating data in existing secret", "namespace", secret.Namespace, "name", secret.Name) // Actually update the secret in the regeneration case (the secret already exists but we want to update to new secretData). _, updateErr := secretClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) return updateErr @@ -169,7 +169,7 @@ func NewExampleCreatingController( utilruntime.HandleError(fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err)) return false } - klog.V(4).InfoS("recreating secret", "namespace", service.Namespace, "name", service.Name) + plog.Debug("recreating secret", "namespace", service.Namespace, "name", service.Name) return true }, }, controllerlib.InformerOption{}), diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index 21326ceb..eccbe2e6 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -11,7 +11,6 @@ import ( k8sinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2/klogr" "k8s.io/utils/clock" pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" @@ -34,6 +33,7 @@ import ( "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/leaderelection" + "go.pinniped.dev/internal/plog" ) const ( @@ -197,6 +197,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { controllerlib.WithInformer, c.ServingCertRenewBefore, apicerts.TLSCertificateChainSecretKey, + plog.New(), ), singletonWorker, ). @@ -222,7 +223,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { agentConfig, client, informers.installationNamespaceK8s.Core().V1().Pods(), - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -232,7 +233,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { webhookcachefiller.New( c.AuthenticatorCache, informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -240,7 +241,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { jwtcachefiller.New( c.AuthenticatorCache, informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -249,7 +250,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { c.AuthenticatorCache, informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -275,7 +276,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { impersonator.New, c.NamesConfig.ImpersonationSignerSecret, c.ImpersonationSigningCertProvider, - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements ), singletonWorker, ). @@ -303,6 +304,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { controllerlib.WithInformer, 365*24*time.Hour-time.Hour, // 1 year minus 1 hour hard coded value (i.e. wait until the last moment to break the signer) apicerts.CACertificateSecretKey, + plog.New(), ), singletonWorker, ) diff --git a/internal/crypto/ptls/fips_strict.go b/internal/crypto/ptls/fips_strict.go index fa79f2c7..c132401e 100644 --- a/internal/crypto/ptls/fips_strict.go +++ b/internal/crypto/ptls/fips_strict.go @@ -20,7 +20,8 @@ import ( _ "crypto/tls/fipsonly" // restricts all TLS configuration to FIPS-approved settings. "k8s.io/apiserver/pkg/server/options" - "k8s.io/klog/v2" + + "go.pinniped.dev/internal/plog" ) // Always use TLS 1.2 for FIPs @@ -36,9 +37,7 @@ func init() { // this init runs before we have parsed our config to determine our log level // thus we must use a log statement that will always print instead of conditionally print - // for plog, that is only error and warning logs, neither of which seem appropriate here - // therefore, just use klog directly with no V level requirement - klog.InfoS("using boring crypto in fips only mode", "go version", runtime.Version()) + plog.Always("using boring crypto in fips only mode", "go version", runtime.Version()) } func Default(rootCAs *x509.CertPool) *tls.Config { diff --git a/internal/groupsuffix/groupsuffix.go b/internal/groupsuffix/groupsuffix.go index c05c3d7a..1dc3397c 100644 --- a/internal/groupsuffix/groupsuffix.go +++ b/internal/groupsuffix/groupsuffix.go @@ -16,7 +16,6 @@ import ( loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/kubeclient" - "go.pinniped.dev/internal/plog" ) const ( @@ -86,8 +85,8 @@ func New(apiGroupSuffix string) kubeclient.Middleware { // want our middleware to be opinionated so that it can be really good at a specific task // and give us specific feedback when it can't do that specific task return fmt.Errorf( - "cannot replace token credential request %q without authenticator API group", - plog.KObj(obj), + "cannot replace token credential request %s/%s without authenticator API group", + obj.GetNamespace(), obj.GetName(), ) } @@ -95,8 +94,8 @@ func New(apiGroupSuffix string) kubeclient.Middleware { if !ok { // see comment above about specificity of middleware return fmt.Errorf( - "cannot replace token credential request %q authenticator API group %q with group suffix %q", - plog.KObj(obj), + "cannot replace token credential request %s/%s authenticator API group %q with group suffix %q", + obj.GetNamespace(), obj.GetName(), *tokenCredentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix, ) @@ -176,7 +175,7 @@ func Unreplace(baseAPIGroup, apiGroupSuffix string) (string, bool) { // makes sure that the provided apiGroupSuffix is a valid DNS-1123 subdomain with at least one dot, // to match Kubernetes behavior. func Validate(apiGroupSuffix string) error { - var errs []error //nolint: prealloc + var errs []error // nolint: prealloc if len(strings.Split(apiGroupSuffix, ".")) < 2 { errs = append(errs, constable.Error("must contain '.'")) diff --git a/internal/groupsuffix/groupsuffix_test.go b/internal/groupsuffix/groupsuffix_test.go index d51119d4..21ff239f 100644 --- a/internal/groupsuffix/groupsuffix_test.go +++ b/internal/groupsuffix/groupsuffix_test.go @@ -435,7 +435,7 @@ func TestMiddlware(t *testing.T) { responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec wantMutateRequests: 3, wantMutateResponses: 1, - wantMutateRequestErrors: []string{`cannot replace token credential request "/" authenticator API group "authentication.concierge.some.suffix.com" with group suffix "some.suffix.com"`}, + wantMutateRequestErrors: []string{`cannot replace token credential request / authenticator API group "authentication.concierge.some.suffix.com" with group suffix "some.suffix.com"`}, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us }, { @@ -475,7 +475,7 @@ func TestMiddlware(t *testing.T) { responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec wantMutateRequests: 3, wantMutateResponses: 1, - wantMutateRequestErrors: []string{`cannot replace token credential request "/" without authenticator API group`}, + wantMutateRequestErrors: []string{`cannot replace token credential request / without authenticator API group`}, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us }, { diff --git a/internal/kubeclient/copied.go b/internal/kubeclient/copied.go index cd682937..ef40e939 100644 --- a/internal/kubeclient/copied.go +++ b/internal/kubeclient/copied.go @@ -39,9 +39,9 @@ func glogBody(prefix string, body []byte) { if bytes.IndexFunc(body, func(r rune) bool { return r < 0x0a }) != -1 { - plog.Debug(prefix, "body", hex.Dump(body)) + plog.All(prefix, "body", hex.Dump(body)) } else { - plog.Debug(prefix, "body", string(body)) + plog.All(prefix, "body", string(body)) } } } diff --git a/internal/localuserauthenticator/localuserauthenticator.go b/internal/localuserauthenticator/localuserauthenticator.go index 3cc2f8d2..6ef851df 100644 --- a/internal/localuserauthenticator/localuserauthenticator.go +++ b/internal/localuserauthenticator/localuserauthenticator.go @@ -22,6 +22,7 @@ import ( "os" "os/signal" "strings" + "syscall" "time" "golang.org/x/crypto/bcrypt" @@ -31,7 +32,6 @@ import ( kubeinformers "k8s.io/client-go/informers" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2" "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/controller/apicerts" @@ -339,10 +339,7 @@ func waitForSignal() os.Signal { return <-signalCh } -func run() error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - +func run(ctx context.Context) error { client, err := kubeclient.New() if err != nil { return fmt.Errorf("cannot create k8s client: %w", err) @@ -359,7 +356,7 @@ func run() error { startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers) plog.Debug("controllers are ready") - //nolint: gosec // Intentionally binding to all network interfaces. + // nolint: gosec // Intentionally binding to all network interfaces. l, err := net.Listen("tcp", ":8443") if err != nil { return fmt.Errorf("cannot create listener: %w", err) @@ -378,13 +375,35 @@ func run() error { return nil } -func Main() { +func main() error { // return an error instead of plog.Fatal to allow defer statements to run + ctx := signalCtx() + // Hardcode the logging level to debug, since this is a test app and it is very helpful to have // verbose logs to debug test failures. - if err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug); err != nil { - klog.Fatal(err) + if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug}); err != nil { + plog.Fatal(err) } - if err := run(); err != nil { - klog.Fatal(err) + + return run(ctx) +} + +func Main() { + if err := main(); err != nil { + plog.Fatal(err) } } + +func signalCtx() context.Context { + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + defer cancel() + + s := <-signalCh + plog.Debug("saw signal", "signal", s) + }() + + return ctx +} diff --git a/internal/plog/config.go b/internal/plog/config.go new file mode 100644 index 00000000..dd5d1f61 --- /dev/null +++ b/internal/plog/config.go @@ -0,0 +1,107 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plog + +import ( + "context" + "encoding/json" + "strconv" + "time" + + "go.uber.org/zap/zapcore" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/component-base/logs" + + "go.pinniped.dev/internal/constable" +) + +type LogFormat string + +func (l *LogFormat) UnmarshalJSON(b []byte) error { + switch string(b) { + case `""`, `"json"`: + *l = FormatJSON + case `"text"`: + *l = FormatText + // there is no "cli" case because it is not a supported option via our config + default: + return errInvalidLogFormat + } + return nil +} + +const ( + FormatJSON LogFormat = "json" + FormatText LogFormat = "text" + FormatCLI LogFormat = "cli" // only used by the pinniped CLI and not the server components + + errInvalidLogLevel = constable.Error("invalid log level, valid choices are the empty string, info, debug, trace and all") + errInvalidLogFormat = constable.Error("invalid log format, valid choices are the empty string, json and text") +) + +var _ json.Unmarshaler = func() *LogFormat { + var f LogFormat + return &f +}() + +type LogSpec struct { + Level LogLevel `json:"level,omitempty"` + Format LogFormat `json:"format,omitempty"` +} + +func MaybeSetDeprecatedLogLevel(level *LogLevel, log *LogSpec) { + if level != nil { + Warning("logLevel is deprecated, set log.level instead") + log.Level = *level + } +} + +func ValidateAndSetLogLevelAndFormatGlobally(ctx context.Context, spec LogSpec) error { + klogLevel := klogLevelForPlogLevel(spec.Level) + if klogLevel < 0 { + return errInvalidLogLevel + } + + // set the global log levels used by our code and the kube code underneath us + if _, err := logs.GlogSetter(strconv.Itoa(int(klogLevel))); err != nil { + panic(err) // programmer error + } + globalLevel.SetLevel(zapcore.Level(-klogLevel)) // klog levels are inverted when zap handles them + + var encoding string + switch spec.Format { + case "", FormatJSON: + encoding = "json" + case FormatCLI: + encoding = "console" + case FormatText: + encoding = "text" + default: + return errInvalidLogFormat + } + + log, flush, err := newLogr(ctx, encoding, klogLevel) + if err != nil { + return err + } + + setGlobalLoggers(log, flush) + + // nolint: exhaustive // the switch above is exhaustive for format already + switch spec.Format { + case FormatCLI: + return nil // do not spawn go routines on the CLI to allow the CLI to call this more than once + case FormatText: + Warning("setting log.format to 'text' is deprecated - this option will be removed in a future release") + } + + // do spawn go routines on the server + go wait.UntilWithContext(ctx, func(_ context.Context) { flush() }, time.Minute) + go func() { + <-ctx.Done() + flush() // best effort flush before shutdown as this is not coordinated with a wait group + }() + + return nil +} diff --git a/internal/plog/config_test.go b/internal/plog/config_test.go new file mode 100644 index 00000000..2cdd9f57 --- /dev/null +++ b/internal/plog/config_test.go @@ -0,0 +1,359 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plog + +import ( + "bufio" + "bytes" + "context" + "fmt" + "os" + "runtime" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "k8s.io/component-base/logs" + "k8s.io/klog/v2" + "k8s.io/klog/v2/textlogger" + clocktesting "k8s.io/utils/clock/testing" +) + +func TestFormat(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + var buf bytes.Buffer + + scanner := bufio.NewScanner(&buf) + + now, err := time.Parse(time.RFC3339Nano, "2022-11-21T23:37:26.953313745Z") + require.NoError(t, err) + fakeClock := clocktesting.NewFakeClock(now) + nowStr := now.Local().Format(time.RFC1123) + + ctx = TestZapOverrides(ctx, t, &buf, nil, zap.WithClock(ZapClock(fakeClock))) + + err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug}) + require.NoError(t, err) + + wd, err := os.Getwd() + require.NoError(t, err) + + const startLogLine = 46 // make this match the current line number + + Info("hello", "happy", "day", "duration", time.Hour+time.Minute) + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.JSONEq(t, fmt.Sprintf(` +{ + "level": "info", + "timestamp": "2022-11-21T23:37:26.953313Z", + "caller": "%s/config_test.go:%d$plog.TestFormat", + "message": "hello", + "happy": "day", + "duration": "1h1m0s" +}`, wd, startLogLine+2), scanner.Text()) + + Logr().WithName("burrito").Error(errInvalidLogLevel, "wee", "a", "b") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.JSONEq(t, fmt.Sprintf(` +{ + "level": "error", + "timestamp": "2022-11-21T23:37:26.953313Z", + "caller": "%s/config_test.go:%d$plog.TestFormat", + "message": "wee", + "a": "b", + "error": "invalid log level, valid choices are the empty string, info, debug, trace and all", + "logger": "burrito" +}`, wd, startLogLine+2+13), scanner.Text()) + + Logr().V(klogLevelWarning).Info("hey") // note that this fails to set the custom warning key because it is not via plog + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.JSONEq(t, fmt.Sprintf(` +{ + "level": "info", + "timestamp": "2022-11-21T23:37:26.953313Z", + "caller": "%s/config_test.go:%d$plog.TestFormat", + "message": "hey" +}`, wd, startLogLine+2+13+14), scanner.Text()) + + Warning("bad stuff") // note that this sets the custom warning key because it is via plog + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.JSONEq(t, fmt.Sprintf(` +{ + "level": "info", + "timestamp": "2022-11-21T23:37:26.953313Z", + "caller": "%s/config_test.go:%d$plog.TestFormat", + "message": "bad stuff", + "warning": true +}`, wd, startLogLine+2+13+14+11), scanner.Text()) + + func() { DebugErr("something happened", errInvalidLogFormat, "an", "item") }() + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.JSONEq(t, fmt.Sprintf(` +{ + "level": "debug", + "timestamp": "2022-11-21T23:37:26.953313Z", + "caller": "%s/config_test.go:%d$plog.TestFormat.func1", + "message": "something happened", + "error": "invalid log format, valid choices are the empty string, json and text", + "an": "item" +}`, wd, startLogLine+2+13+14+11+12), scanner.Text()) + + Trace("should not be logged", "for", "sure") + require.Empty(t, buf.String()) + + Logr().V(klogLevelAll).Info("also should not be logged", "open", "close") + require.Empty(t, buf.String()) + + ctx = TestZapOverrides(ctx, t, &buf, nil, zap.WithClock(ZapClock(fakeClock)), zap.AddStacktrace(LevelInfo)) + + err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug}) + require.NoError(t, err) + + WithName("stacky").WithName("does").Info("has a stack trace!") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.JSONEq(t, fmt.Sprintf(` +{ + "level": "info", + "timestamp": "2022-11-21T23:37:26.953313Z", + "caller": "%s/config_test.go:%d$plog.TestFormat", + "message": "has a stack trace!", + "logger": "stacky.does", + "stacktrace": %s +}`, wd, startLogLine+2+13+14+11+12+24, + strconv.Quote( + fmt.Sprintf( + `go.pinniped.dev/internal/plog.TestFormat + %s/config_test.go:%d +testing.tRunner + %s/src/testing/testing.go:1439`, + wd, startLogLine+2+13+14+11+12+24, runtime.GOROOT(), + ), + ), + ), scanner.Text()) + + ctx = TestZapOverrides(ctx, t, &buf, nil, zap.WithClock(ZapClock(fakeClock))) + + err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug, Format: FormatCLI}) + require.NoError(t, err) + + DebugErr("something happened", errInvalidLogFormat, "an", "item") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(nowStr+` plog/config_test.go:%d something happened {"error": "invalid log format, valid choices are the empty string, json and text", "an": "item"}`, + startLogLine+2+13+14+11+12+24+28), scanner.Text()) + + Logr().WithName("burrito").Error(errInvalidLogLevel, "wee", "a", "b", "slightly less than a year", 363*24*time.Hour, "slightly more than 2 years", 2*367*24*time.Hour) + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(nowStr+` burrito plog/config_test.go:%d wee {"a": "b", "slightly less than a year": "363d", "slightly more than 2 years": "2y4d", "error": "invalid log level, valid choices are the empty string, info, debug, trace and all"}`, + startLogLine+2+13+14+11+12+24+28+6), scanner.Text()) + + origTimeNow := textlogger.TimeNow + t.Cleanup(func() { + textlogger.TimeNow = origTimeNow + }) + textlogger.TimeNow = func() time.Time { + return now + } + + old := New().WithName("created before mode change").WithValues("is", "old") + + err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug, Format: FormatText}) + require.NoError(t, err) + pid := os.Getpid() + + // check for the deprecation warning + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config.go:96] "setting log.format to 'text' is deprecated - this option will be removed in a future release" warning=true`, + pid), scanner.Text()) + + Debug("what is happening", "does klog", "work?") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "what is happening" does klog="work?"`, + pid, startLogLine+2+13+14+11+12+24+28+6+26), scanner.Text()) + + Logr().WithName("panda").V(KlogLevelDebug).Info("are the best", "yes?", "yes.") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "panda: are the best" yes?="yes."`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6), scanner.Text()) + + New().WithName("hi").WithName("there").WithValues("a", 1, "b", 2).Always("do it") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "hi/there: do it" a=1 b=2`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6), scanner.Text()) + + l := WithValues("x", 33, "z", 22) + l.Debug("what to do") + l.Debug("and why") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "what to do" x=33 z=22`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7), scanner.Text()) + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "and why" x=33 z=22`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1), scanner.Text()) + + old.Always("should be klog text format", "for", "sure") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "created before mode change: should be klog text format" is="old" for="sure"`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1+10), scanner.Text()) + + // make sure child loggers do not share state + old1 := old.WithValues("i am", "old1") + old2 := old.WithName("old2") + old1.Warning("warn") + old2.Info("info") + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "created before mode change: warn" is="old" i am="old1" warning=true`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1+10+9), scanner.Text()) + require.True(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "created before mode change/old2: info" is="old"`, + pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1+10+9+1), scanner.Text()) + + Trace("should not be logged", "for", "sure") + require.Empty(t, buf.String()) + + Logr().V(klogLevelAll).Info("also should not be logged", "open", "close") + require.Empty(t, buf.String()) + + require.False(t, scanner.Scan()) + require.NoError(t, scanner.Err()) + require.Empty(t, scanner.Text()) + require.Empty(t, buf.String()) +} + +func TestValidateAndSetLogLevelGlobally(t *testing.T) { + originalLogLevel := getKlogLevel() + require.GreaterOrEqual(t, int(originalLogLevel), int(klog.Level(0)), "cannot get klog level") + + tests := []struct { + name string + level LogLevel + wantLevel klog.Level + wantEnabled []LogLevel + wantErr string + }{ + { + name: "unset", + wantLevel: 0, + wantEnabled: []LogLevel{LevelWarning}, + }, + { + name: "warning", + level: LevelWarning, + wantLevel: 0, + wantEnabled: []LogLevel{LevelWarning}, + }, + { + name: "info", + level: LevelInfo, + wantLevel: 2, + wantEnabled: []LogLevel{LevelWarning, LevelInfo}, + }, + { + name: "debug", + level: LevelDebug, + wantLevel: 4, + wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug}, + }, + { + name: "trace", + level: LevelTrace, + wantLevel: 6, + wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace}, + }, + { + name: "all", + level: LevelAll, + wantLevel: 108, + wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll}, + }, + { + name: "invalid level", + level: "panda", + wantLevel: originalLogLevel, + wantErr: errInvalidLogLevel.Error(), + }, + } + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + defer func() { + undoGlobalLogLevelChanges(t, originalLogLevel) + }() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + err := ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: tt.level}) + require.Equal(t, tt.wantErr, errString(err)) + require.Equal(t, tt.wantLevel, getKlogLevel()) + + if tt.wantEnabled != nil { + allLevels := []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll} + for _, level := range allLevels { + if contains(tt.wantEnabled, level) { + require.Truef(t, Enabled(level), "wanted %q to be enabled", level) + } else { + require.False(t, Enabled(level), "did not want %q to be enabled", level) + } + } + } + }) + } + + require.Equal(t, originalLogLevel, getKlogLevel()) +} + +func contains(haystack []LogLevel, needle LogLevel) bool { + for _, hay := range haystack { + if hay == needle { + return true + } + } + return false +} + +func errString(err error) string { + if err == nil { + return "" + } + + return err.Error() +} + +func undoGlobalLogLevelChanges(t *testing.T, originalLogLevel klog.Level) { + t.Helper() + _, err := logs.GlogSetter(strconv.Itoa(int(originalLogLevel))) + require.NoError(t, err) +} + +func getKlogLevel() klog.Level { + // hack around klog not exposing a Get method + for i := klog.Level(0); i < 256; i++ { + if klog.V(i).Enabled() { + continue + } + return i - 1 + } + + return -1 +} diff --git a/internal/plog/global.go b/internal/plog/global.go new file mode 100644 index 00000000..dc4dfbcd --- /dev/null +++ b/internal/plog/global.go @@ -0,0 +1,77 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plog + +import ( + "context" + "fmt" + "net/url" + "sync" + + "github.com/go-logr/logr" + "go.uber.org/zap" + "k8s.io/component-base/logs" + "k8s.io/klog/v2" +) + +// nolint: gochecknoglobals +var ( + // note that these globals have no locks on purpose - they are expected to be set at init and then again after config parsing. + globalLevel zap.AtomicLevel + globalLogger logr.Logger + globalFlush func() + + // used as a temporary storage for a buffer per call of newLogr. see the init function below for more details. + sinkMap sync.Map +) + +// nolint: gochecknoinits +func init() { + // make sure we always have a functional global logger + globalLevel = zap.NewAtomicLevelAt(0) // log at the 0 verbosity level to start with, i.e. the "always" logs + // use json encoding to start with + // the context here is just used for test injection and thus can be ignored + log, flush, err := newLogr(context.Background(), "json", 0) + if err != nil { + panic(err) // default logging config must always work + } + setGlobalLoggers(log, flush) + + // this is a little crazy but zap's builder code does not allow us to directly specify what + // writer we want to use as our log sink. to get around this limitation in tests, we use a + // global map to temporarily hold the writer (the key is a random string that is generated + // per invocation of newLogr). we register a fake "pinniped" scheme so that we can lookup + // the writer via pinniped:///. + if err := zap.RegisterSink("pinniped", func(u *url.URL) (zap.Sink, error) { + value, ok := sinkMap.Load(u.Path) + if !ok { + return nil, fmt.Errorf("key %q not in global sink", u.Path) + } + return value.(zap.Sink), nil + }); err != nil { + panic(err) // custom sink must always work + } +} + +// Deprecated: Use New instead. This is meant for old code only. +// New provides a more ergonomic API and correctly responds to global log config change. +func Logr() logr.Logger { + return globalLogger +} + +func Setup() func() { + logs.InitLogs() + return func() { + logs.FlushLogs() + globalFlush() + } +} + +// setGlobalLoggers sets the plog and klog global loggers. it is *not* go routine safe. +func setGlobalLoggers(log logr.Logger, flush func()) { + // a contextual logger does its own level based enablement checks, which is true for all of our loggers + klog.SetLoggerWithOptions(log, klog.ContextualLogger(true), klog.FlushLogger(flush)) + globalLogger = log + globalFlush = flush +} diff --git a/internal/plog/klog.go b/internal/plog/klog.go deleted file mode 100644 index cd62b6c6..00000000 --- a/internal/plog/klog.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package plog - -import ( - "fmt" - - "k8s.io/klog/v2" -) - -// KObj is (mostly) copied from klog - it is a standard way to represent a metav1.Object in logs. -func KObj(obj klog.KMetadata) string { - return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()) -} - -func klogLevelForPlogLevel(plogLevel LogLevel) klog.Level { - switch plogLevel { - case LevelWarning: - return klogLevelWarning // unset means minimal logs (Error and Warning) - case LevelInfo: - return klogLevelInfo - case LevelDebug: - return klogLevelDebug - case LevelTrace: - return klogLevelTrace - case LevelAll: - return klogLevelAll + 100 // make all really mean all - default: - return -1 - } -} diff --git a/internal/plog/level.go b/internal/plog/level.go index 0f8401d6..4daa7b93 100644 --- a/internal/plog/level.go +++ b/internal/plog/level.go @@ -4,18 +4,18 @@ package plog import ( - "strconv" - - "k8s.io/component-base/logs" + "go.uber.org/zap/zapcore" "k8s.io/klog/v2" - - "go.pinniped.dev/internal/constable" ) // LogLevel is an enum that controls verbosity of logs. // Valid values in order of increasing verbosity are leaving it unset, info, debug, trace and all. type LogLevel string +func (l LogLevel) Enabled(_ zapcore.Level) bool { + return Enabled(l) // this basically says "log if the global plog level is l or greater" +} + const ( // LevelWarning (i.e. leaving the log level unset) maps to klog log level 0. LevelWarning LogLevel = "" @@ -27,33 +27,40 @@ const ( LevelTrace LogLevel = "trace" // LevelAll maps to klog log level 100 (conceptually it is log level 8). LevelAll LogLevel = "all" - - errInvalidLogLevel = constable.Error("invalid log level, valid choices are the empty string, info, debug, trace and all") ) +var _ zapcore.LevelEnabler = LevelWarning + const ( klogLevelWarning = iota * 2 - klogLevelInfo - klogLevelDebug - klogLevelTrace + KlogLevelInfo + KlogLevelDebug + KlogLevelTrace klogLevelAll ) -func ValidateAndSetLogLevelGlobally(level LogLevel) error { - klogLevel := klogLevelForPlogLevel(level) - if klogLevel < 0 { - return errInvalidLogLevel - } - - if _, err := logs.GlogSetter(strconv.Itoa(int(klogLevel))); err != nil { - panic(err) // programmer error - } - - return nil -} - // Enabled returns whether the provided plog level is enabled, i.e., whether print statements at the // provided level will show up. func Enabled(level LogLevel) bool { - return klog.V(klogLevelForPlogLevel(level)).Enabled() + l := klogLevelForPlogLevel(level) + // check that both our global level and the klog global level agree that the plog level is enabled + // klog levels are inverted when zap handles them + return globalLevel.Enabled(zapcore.Level(-l)) && klog.V(l).Enabled() +} + +func klogLevelForPlogLevel(plogLevel LogLevel) klog.Level { + switch plogLevel { + case LevelWarning: + return klogLevelWarning // unset means minimal logs (Error and Warning) + case LevelInfo: + return KlogLevelInfo + case LevelDebug: + return KlogLevelDebug + case LevelTrace: + return KlogLevelTrace + case LevelAll: + return klogLevelAll + 100 // make all really mean all + default: + return -1 + } } diff --git a/internal/plog/level_test.go b/internal/plog/level_test.go deleted file mode 100644 index 9da066f4..00000000 --- a/internal/plog/level_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package plog - -import ( - "strconv" - "testing" - - "github.com/stretchr/testify/require" - "k8s.io/component-base/logs" - "k8s.io/klog/v2" -) - -func TestValidateAndSetLogLevelGlobally(t *testing.T) { - originalLogLevel := getKlogLevel() - require.GreaterOrEqual(t, int(originalLogLevel), int(klog.Level(0)), "cannot get klog level") - - tests := []struct { - name string - level LogLevel - wantLevel klog.Level - wantEnabled []LogLevel - wantErr string - }{ - { - name: "unset", - wantLevel: 0, - wantEnabled: []LogLevel{LevelWarning}, - }, - { - name: "warning", - level: LevelWarning, - wantLevel: 0, - wantEnabled: []LogLevel{LevelWarning}, - }, - { - name: "info", - level: LevelInfo, - wantLevel: 2, - wantEnabled: []LogLevel{LevelWarning, LevelInfo}, - }, - { - name: "debug", - level: LevelDebug, - wantLevel: 4, - wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug}, - }, - { - name: "trace", - level: LevelTrace, - wantLevel: 6, - wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace}, - }, - { - name: "all", - level: LevelAll, - wantLevel: 108, - wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll}, - }, - { - name: "invalid level", - level: "panda", - wantLevel: originalLogLevel, - wantErr: errInvalidLogLevel.Error(), - }, - } - for _, tt := range tests { - tt := tt // capture range variable - t.Run(tt.name, func(t *testing.T) { - defer func() { - undoGlobalLogLevelChanges(t, originalLogLevel) - }() - - err := ValidateAndSetLogLevelGlobally(tt.level) - require.Equal(t, tt.wantErr, errString(err)) - require.Equal(t, tt.wantLevel, getKlogLevel()) - - if tt.wantEnabled != nil { - allLevels := []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll} - for _, level := range allLevels { - if contains(tt.wantEnabled, level) { - require.Truef(t, Enabled(level), "wanted %q to be enabled", level) - } else { - require.False(t, Enabled(level), "did not want %q to be enabled", level) - } - } - } - }) - } - - require.Equal(t, originalLogLevel, getKlogLevel()) -} - -func contains(haystack []LogLevel, needle LogLevel) bool { - for _, hay := range haystack { - if hay == needle { - return true - } - } - return false -} - -func errString(err error) string { - if err == nil { - return "" - } - - return err.Error() -} - -func undoGlobalLogLevelChanges(t *testing.T, originalLogLevel klog.Level) { - t.Helper() - _, err := logs.GlogSetter(strconv.Itoa(int(originalLogLevel))) - require.NoError(t, err) -} - -func getKlogLevel() klog.Level { - // hack around klog not exposing a Get method - for i := klog.Level(0); i < 256; i++ { - if klog.V(i).Enabled() { - continue - } - return i - 1 - } - - return -1 -} diff --git a/internal/plog/plog.go b/internal/plog/plog.go index 5fbfb512..060e7036 100644 --- a/internal/plog/plog.go +++ b/internal/plog/plog.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package plog implements a thin layer over klog to help enforce pinniped's logging convention. +// Package plog implements a thin layer over logr to help enforce pinniped's logging convention. // Logs are always structured as a constant message with key and value pairs of related metadata. // // The logging levels in order of increasing verbosity are: @@ -26,10 +26,18 @@ // act of desperation to determine why the system is broken. package plog -import "k8s.io/klog/v2" +import ( + "os" -const errorKey = "error" + "github.com/go-logr/logr" +) +const errorKey = "error" // this matches zapr's default for .Error calls (which is asserted via tests) + +// Logger implements the plog logging convention described above. The global functions in this package +// such as Info should be used when one does not intend to write tests assertions for specific log messages. +// If test assertions are desired, Logger should be passed in as an input. New should be used as the +// production implementation and TestLogger should be used to write test assertions. type Logger interface { Error(msg string, err error, keysAndValues ...interface{}) Warning(msg string, keysAndValues ...interface{}) @@ -41,100 +49,155 @@ type Logger interface { Trace(msg string, keysAndValues ...interface{}) TraceErr(msg string, err error, keysAndValues ...interface{}) All(msg string, keysAndValues ...interface{}) + Always(msg string, keysAndValues ...interface{}) + WithValues(keysAndValues ...interface{}) Logger + WithName(name string) Logger + + // does not include Fatal on purpose because that is not a method you should be using + + // for internal and test use only + withDepth(d int) Logger + withLogrMod(mod func(logr.Logger) logr.Logger) Logger } +// MinLogger is the overlap between Logger and logr.Logger. +type MinLogger interface { + Info(msg string, keysAndValues ...interface{}) +} + +var _ Logger = pLogger{} +var _, _, _ MinLogger = pLogger{}, logr.Logger{}, Logger(nil) + type pLogger struct { - prefix string - depth int + mods []func(logr.Logger) logr.Logger + depth int } -func New(prefix string) Logger { - return &pLogger{ - depth: 0, - prefix: prefix, +func New() Logger { + return pLogger{} +} + +func (p pLogger) Error(msg string, err error, keysAndValues ...interface{}) { + p.logr().WithCallDepth(p.depth+1).Error(err, msg, keysAndValues...) +} + +func (p pLogger) warningDepth(msg string, depth int, keysAndValues ...interface{}) { + if p.logr().V(klogLevelWarning).Enabled() { + // klog's structured logging has no concept of a warning (i.e. no WarningS function) + // Thus we use info at log level zero as a proxy + // klog's info logs have an I prefix and its warning logs have a W prefix + // Since we lose the W prefix by using InfoS, just add a key to make these easier to find + keysAndValues = append([]interface{}{"warning", true}, keysAndValues...) + p.logr().V(klogLevelWarning).WithCallDepth(depth+1).Info(msg, keysAndValues...) } } -func (p *pLogger) Error(msg string, err error, keysAndValues ...interface{}) { - klog.ErrorSDepth(p.depth+1, err, p.prefix+msg, keysAndValues...) -} - -func (p *pLogger) warningDepth(msg string, depth int, keysAndValues ...interface{}) { - // klog's structured logging has no concept of a warning (i.e. no WarningS function) - // Thus we use info at log level zero as a proxy - // klog's info logs have an I prefix and its warning logs have a W prefix - // Since we lose the W prefix by using InfoS, just add a key to make these easier to find - keysAndValues = append([]interface{}{"warning", "true"}, keysAndValues...) - if klog.V(klogLevelWarning).Enabled() { - klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) - } -} - -func (p *pLogger) Warning(msg string, keysAndValues ...interface{}) { +func (p pLogger) Warning(msg string, keysAndValues ...interface{}) { p.warningDepth(msg, p.depth+1, keysAndValues...) } -// Use WarningErr to issue a Warning message with an error object as part of the message. -func (p *pLogger) WarningErr(msg string, err error, keysAndValues ...interface{}) { +func (p pLogger) WarningErr(msg string, err error, keysAndValues ...interface{}) { p.warningDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) } -func (p *pLogger) infoDepth(msg string, depth int, keysAndValues ...interface{}) { - if klog.V(klogLevelInfo).Enabled() { - klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) +func (p pLogger) infoDepth(msg string, depth int, keysAndValues ...interface{}) { + if p.logr().V(KlogLevelInfo).Enabled() { + p.logr().V(KlogLevelInfo).WithCallDepth(depth+1).Info(msg, keysAndValues...) } } -func (p *pLogger) Info(msg string, keysAndValues ...interface{}) { +func (p pLogger) Info(msg string, keysAndValues ...interface{}) { p.infoDepth(msg, p.depth+1, keysAndValues...) } -// Use InfoErr to log an expected error, e.g. validation failure of an http parameter. -func (p *pLogger) InfoErr(msg string, err error, keysAndValues ...interface{}) { +func (p pLogger) InfoErr(msg string, err error, keysAndValues ...interface{}) { p.infoDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) } -func (p *pLogger) debugDepth(msg string, depth int, keysAndValues ...interface{}) { - if klog.V(klogLevelDebug).Enabled() { - klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) +func (p pLogger) debugDepth(msg string, depth int, keysAndValues ...interface{}) { + if p.logr().V(KlogLevelDebug).Enabled() { + p.logr().V(KlogLevelDebug).WithCallDepth(depth+1).Info(msg, keysAndValues...) } } -func (p *pLogger) Debug(msg string, keysAndValues ...interface{}) { +func (p pLogger) Debug(msg string, keysAndValues ...interface{}) { p.debugDepth(msg, p.depth+1, keysAndValues...) } -// Use DebugErr to issue a Debug message with an error object as part of the message. -func (p *pLogger) DebugErr(msg string, err error, keysAndValues ...interface{}) { +func (p pLogger) DebugErr(msg string, err error, keysAndValues ...interface{}) { p.debugDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) } -func (p *pLogger) traceDepth(msg string, depth int, keysAndValues ...interface{}) { - if klog.V(klogLevelTrace).Enabled() { - klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) +func (p pLogger) traceDepth(msg string, depth int, keysAndValues ...interface{}) { + if p.logr().V(KlogLevelTrace).Enabled() { + p.logr().V(KlogLevelTrace).WithCallDepth(depth+1).Info(msg, keysAndValues...) } } -func (p *pLogger) Trace(msg string, keysAndValues ...interface{}) { +func (p pLogger) Trace(msg string, keysAndValues ...interface{}) { p.traceDepth(msg, p.depth+1, keysAndValues...) } -// Use TraceErr to issue a Trace message with an error object as part of the message. -func (p *pLogger) TraceErr(msg string, err error, keysAndValues ...interface{}) { +func (p pLogger) TraceErr(msg string, err error, keysAndValues ...interface{}) { p.traceDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) } -func (p *pLogger) All(msg string, keysAndValues ...interface{}) { - if klog.V(klogLevelAll).Enabled() { - klog.InfoSDepth(p.depth+1, p.prefix+msg, keysAndValues...) +func (p pLogger) All(msg string, keysAndValues ...interface{}) { + if p.logr().V(klogLevelAll).Enabled() { + p.logr().V(klogLevelAll).WithCallDepth(p.depth+1).Info(msg, keysAndValues...) } } -var logger Logger = &pLogger{ //nolint:gochecknoglobals - depth: 1, +func (p pLogger) Always(msg string, keysAndValues ...interface{}) { + p.logr().WithCallDepth(p.depth+1).Info(msg, keysAndValues...) } -// Use Error to log an unexpected system error. +func (p pLogger) WithValues(keysAndValues ...interface{}) Logger { + if len(keysAndValues) == 0 { + return p + } + + return p.withLogrMod(func(l logr.Logger) logr.Logger { + return l.WithValues(keysAndValues...) + }) +} + +func (p pLogger) WithName(name string) Logger { + if len(name) == 0 { + return p + } + + return p.withLogrMod(func(l logr.Logger) logr.Logger { + return l.WithName(name) + }) +} + +func (p pLogger) withDepth(d int) Logger { + out := p + out.depth += d // out is a copy so this does not mutate p + return out +} + +func (p pLogger) withLogrMod(mod func(logr.Logger) logr.Logger) Logger { + out := p // make a copy and carefully avoid mutating the mods slice + mods := make([]func(logr.Logger) logr.Logger, 0, len(out.mods)+1) + mods = append(mods, out.mods...) + mods = append(mods, mod) + out.mods = mods + return out +} + +func (p pLogger) logr() logr.Logger { + l := Logr() // grab the current global logger and its current config + for _, mod := range p.mods { + mod := mod + l = mod(l) // and then update it with all modifications + } + return l // this logger is guaranteed to have the latest config and all modifications +} + +var logger = New().withDepth(1) //nolint:gochecknoglobals + func Error(msg string, err error, keysAndValues ...interface{}) { logger.Error(msg, err, keysAndValues...) } @@ -143,7 +206,6 @@ func Warning(msg string, keysAndValues ...interface{}) { logger.Warning(msg, keysAndValues...) } -// Use WarningErr to issue a Warning message with an error object as part of the message. func WarningErr(msg string, err error, keysAndValues ...interface{}) { logger.WarningErr(msg, err, keysAndValues...) } @@ -152,7 +214,6 @@ func Info(msg string, keysAndValues ...interface{}) { logger.Info(msg, keysAndValues...) } -// Use InfoErr to log an expected error, e.g. validation failure of an http parameter. func InfoErr(msg string, err error, keysAndValues ...interface{}) { logger.InfoErr(msg, err, keysAndValues...) } @@ -161,7 +222,6 @@ func Debug(msg string, keysAndValues ...interface{}) { logger.Debug(msg, keysAndValues...) } -// Use DebugErr to issue a Debug message with an error object as part of the message. func DebugErr(msg string, err error, keysAndValues ...interface{}) { logger.DebugErr(msg, err, keysAndValues...) } @@ -170,7 +230,6 @@ func Trace(msg string, keysAndValues ...interface{}) { logger.Trace(msg, keysAndValues...) } -// Use TraceErr to issue a Trace message with an error object as part of the message. func TraceErr(msg string, err error, keysAndValues ...interface{}) { logger.TraceErr(msg, err, keysAndValues...) } @@ -178,3 +237,23 @@ func TraceErr(msg string, err error, keysAndValues ...interface{}) { func All(msg string, keysAndValues ...interface{}) { logger.All(msg, keysAndValues...) } + +func Always(msg string, keysAndValues ...interface{}) { + logger.Always(msg, keysAndValues...) +} + +func WithValues(keysAndValues ...interface{}) Logger { + // this looks weird but it is the same as New().WithValues(keysAndValues...) because it returns a new logger rooted at the call site + return logger.withDepth(-1).WithValues(keysAndValues...) +} + +func WithName(name string) Logger { + // this looks weird but it is the same as New().WithName(name) because it returns a new logger rooted at the call site + return logger.withDepth(-1).WithName(name) +} + +func Fatal(err error, keysAndValues ...interface{}) { + logger.Error("unrecoverable error encountered", err, keysAndValues...) + globalFlush() + os.Exit(1) +} diff --git a/internal/plog/plog_test.go b/internal/plog/plog_test.go new file mode 100644 index 00000000..d66b8871 --- /dev/null +++ b/internal/plog/plog_test.go @@ -0,0 +1,366 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plog + +import ( + "bytes" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestPlog(t *testing.T) { + tests := []struct { + name string + run func(Logger) + want string + }{ + { + name: "basic", + run: testAllPlogMethods, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","panda":2} +`, + }, + { + name: "with values", + run: func(l Logger) { + testAllPlogMethods(l.WithValues("hi", 42)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","hi":42,"panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","hi":42,"warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","hi":42,"warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","hi":42,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","hi":42,"error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","hi":42,"panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","hi":42,"error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","hi":42,"panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","hi":42,"error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","hi":42,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","hi":42,"panda":2} +`, + }, + { + name: "with values conflict", // duplicate key is included twice ... + run: func(l Logger) { + testAllPlogMethods(l.WithValues("panda", false)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","panda":false,"panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","panda":false,"warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","panda":false,"warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","panda":false,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","panda":false,"error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","panda":false,"panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","panda":false,"error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","panda":false,"panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","panda":false,"error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","panda":false,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","panda":false,"panda":2} +`, + }, + { + name: "with values nested", + run: func(l Logger) { + testAllPlogMethods(l.WithValues("hi", 42).WithValues("not", time.Hour)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","hi":42,"not":"1h0m0s","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","hi":42,"not":"1h0m0s","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","hi":42,"not":"1h0m0s","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","hi":42,"not":"1h0m0s","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","hi":42,"not":"1h0m0s","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","hi":42,"not":"1h0m0s","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","hi":42,"not":"1h0m0s","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","hi":42,"not":"1h0m0s","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","hi":42,"not":"1h0m0s","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","hi":42,"not":"1h0m0s","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","hi":42,"not":"1h0m0s","panda":2} +`, + }, + { + name: "with name", + run: func(l Logger) { + testAllPlogMethods(l.WithName("yoyo")) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","panda":2} +`, + }, + { + name: "with name nested", + run: func(l Logger) { + testAllPlogMethods(l.WithName("yoyo").WithName("gold")) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","panda":2} +`, + }, + { + name: "depth 3", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(3)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:$testing.tRunner","message":"always","panda":2} +`, + }, + { + name: "depth 2", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(2)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func15","message":"always","panda":2} +`, + }, + { + name: "depth 1", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(1)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func8","message":"always","panda":2} +`, + }, + { + name: "depth 0", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(0)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.testAllPlogMethods","message":"always","panda":2} +`, + }, + { + name: "depth -1", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(-1)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Error","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Warning","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.WarningErr","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Info","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.InfoErr","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Debug","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.DebugErr","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Trace","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.TraceErr","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.All","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Always","message":"always","panda":2} +`, + }, + { + name: "depth -2", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(-2)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Error","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.warningDepth","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.warningDepth","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.infoDepth","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.infoDepth","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.debugDepth","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.debugDepth","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.traceDepth","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.traceDepth","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"always","panda":2} +`, + }, + { + name: "depth -3", + run: func(l Logger) { + testAllPlogMethods(l.withDepth(-3)) + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.2.3/zapr.go:$zapr.(*zapLogger).Error","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:$logr.Logger.Info","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.2.3/zapr.go:$zapr.(*zapLogger).Info","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.2.3/zapr.go:$zapr.(*zapLogger).Info","message":"always","panda":2} +`, + }, + { + name: "closure", + run: func(l Logger) { + func() { + func() { + testErr := fmt.Errorf("some err") + + l.Error("e", testErr, "panda", 2) + l.Warning("w", "panda", 2) + l.WarningErr("we", testErr, "panda", 2) + l.Info("i", "panda", 2) + l.InfoErr("ie", testErr, "panda", 2) + l.Debug("d", "panda", 2) + l.DebugErr("de", testErr, "panda", 2) + l.Trace("t", "panda", 2) + l.TraceErr("te", testErr, "panda", 2) + l.All("all", "panda", 2) + l.Always("always", "panda", 2) + }() + }() + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:$plog.TestPlog.func13.1.1","message":"always","panda":2} +`, + }, + { + name: "closure depth -1", + run: func(l Logger) { + func() { + func() { + testErr := fmt.Errorf("some err") + + l = l.withDepth(-1) + l.Error("e", testErr, "panda", 2) + l.Warning("w", "panda", 2) + l.WarningErr("we", testErr, "panda", 2) + l.Info("i", "panda", 2) + l.InfoErr("ie", testErr, "panda", 2) + l.Debug("d", "panda", 2) + l.DebugErr("de", testErr, "panda", 2) + l.Trace("t", "panda", 2) + l.TraceErr("te", testErr, "panda", 2) + l.All("all", "panda", 2) + l.Always("always", "panda", 2) + }() + }() + }, + want: ` +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Error","message":"e","panda":2,"error":"some err"} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Warning","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.WarningErr","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Info","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.InfoErr","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Debug","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.DebugErr","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Trace","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.TraceErr","message":"te","error":"some err","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.All","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.Always","message":"always","panda":2} +`, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var log bytes.Buffer + tt.run(TestLogger(t, &log)) + + require.Equal(t, strings.TrimSpace(tt.want), strings.TrimSpace(log.String())) + }) + } +} + +func testAllPlogMethods(l Logger) { + testErr := fmt.Errorf("some err") + + l.Error("e", testErr, "panda", 2) + l.Warning("w", "panda", 2) + l.WarningErr("we", testErr, "panda", 2) + l.Info("i", "panda", 2) + l.InfoErr("ie", testErr, "panda", 2) + l.Debug("d", "panda", 2) + l.DebugErr("de", testErr, "panda", 2) + l.Trace("t", "panda", 2) + l.TraceErr("te", testErr, "panda", 2) + l.All("all", "panda", 2) + l.Always("always", "panda", 2) +} diff --git a/internal/plog/testing.go b/internal/plog/testing.go new file mode 100644 index 00000000..f5c1fea4 --- /dev/null +++ b/internal/plog/testing.go @@ -0,0 +1,120 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plog + +import ( + "context" + "io" + "math" + "strings" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "k8s.io/utils/clock" + clocktesting "k8s.io/utils/clock/testing" +) + +// contextKey type is unexported to prevent collisions. +type contextKey int + +const zapOverridesKey contextKey = iota + +func TestZapOverrides(ctx context.Context, t *testing.T, w io.Writer, f func(*zap.Config), opts ...zap.Option) context.Context { + t.Helper() // discourage use outside of tests + + overrides := &testOverrides{ + t: t, + w: w, + f: f, + opts: opts, + } + return context.WithValue(ctx, zapOverridesKey, overrides) +} + +func TestLogger(t *testing.T, w io.Writer) Logger { + t.Helper() + + return New().withLogrMod(func(l logr.Logger) logr.Logger { + return l.WithSink(TestZapr(t, w).GetSink()) + }) +} + +func TestZapr(t *testing.T, w io.Writer) logr.Logger { + t.Helper() + + now, err := time.Parse(time.RFC3339Nano, "2099-08-08T13:57:36.123456789Z") + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + ctx = TestZapOverrides(ctx, t, w, + func(config *zap.Config) { + config.Level = zap.NewAtomicLevelAt(math.MinInt8) // log everything during tests + + // make test assertions less painful to write while keeping them as close to the real thing as possible + config.EncoderConfig.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { + trimmed := caller.TrimmedPath() + if idx := strings.LastIndexByte(trimmed, ':'); idx != -1 { + trimmed = trimmed[:idx+1] + "" + } + enc.AppendString(trimmed + funcEncoder(caller)) + } + }, + zap.WithClock(ZapClock(clocktesting.NewFakeClock(now))), // have the clock be static during tests + zap.AddStacktrace(nopLevelEnabler{}), // do not log stacktraces + ) + + // there is no buffering so we can ignore flush + zl, _, err := newLogr(ctx, "json", 0) + require.NoError(t, err) + + return zl +} + +var _ zapcore.Clock = &clockAdapter{} + +type clockAdapter struct { + clock clock.Clock +} + +func (c *clockAdapter) Now() time.Time { + return c.clock.Now() +} + +func (c *clockAdapter) NewTicker(duration time.Duration) *time.Ticker { + return &time.Ticker{C: c.clock.Tick(duration)} +} + +func ZapClock(c clock.Clock) zapcore.Clock { + return &clockAdapter{clock: c} +} + +var _ zap.Sink = nopCloserSink{} + +type nopCloserSink struct{ zapcore.WriteSyncer } + +func (nopCloserSink) Close() error { return nil } + +// newSink returns a wrapper around the input writer that is safe for concurrent use. +func newSink(w io.Writer) zap.Sink { + return nopCloserSink{WriteSyncer: zapcore.Lock(zapcore.AddSync(w))} +} + +var _ zapcore.LevelEnabler = nopLevelEnabler{} + +type nopLevelEnabler struct{} + +func (nopLevelEnabler) Enabled(_ zapcore.Level) bool { return false } + +type testOverrides struct { + t *testing.T + w io.Writer + f func(*zap.Config) + opts []zap.Option +} diff --git a/internal/plog/zap.go b/internal/plog/zap.go new file mode 100644 index 00000000..2b9976ef --- /dev/null +++ b/internal/plog/zap.go @@ -0,0 +1,187 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plog + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/klog/v2" + "k8s.io/klog/v2/textlogger" +) + +func newLogr(ctx context.Context, encoding string, klogLevel klog.Level) (logr.Logger, func(), error) { + if encoding == "text" { + var w io.Writer = os.Stderr + flush := func() { _ = os.Stderr.Sync() } + + // allow tests to override klog config (but cheat and re-use the zap override key) + if overrides, ok := ctx.Value(zapOverridesKey).(*testOverrides); ok { + if overrides.w != nil { + w = newSink(overrides.w) // make sure the value is safe for concurrent use + flush = func() {} + } + } + + return textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(int(klogLevel)), textlogger.Output(w))), flush, nil + } + + path := "stderr" // this is how zap refers to os.Stderr + f := func(config *zap.Config) { + if encoding == "console" { + config.EncoderConfig.LevelKey = zapcore.OmitKey + config.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder + config.EncoderConfig.EncodeTime = humanTimeEncoder + config.EncoderConfig.EncodeDuration = humanDurationEncoder + } + } + var opts []zap.Option + + // allow tests to override zap config + if overrides, ok := ctx.Value(zapOverridesKey).(*testOverrides); ok { + if overrides.w != nil { + // use a per invocation random string as the key into the global map + testKey := "/" + base64.RawURLEncoding.EncodeToString([]byte(rand.String(32))) + + // tell zap to use our custom sink registry to find the writer + path = "pinniped://" + testKey + + // the registry may be called multiple times so make sure the value is safe for concurrent use + sink := newSink(overrides.w) + + // store the test's buffer where we can find it globally + actual, loaded := sinkMap.LoadOrStore(testKey, sink) + require.False(overrides.t, loaded) + require.Equal(overrides.t, sink, actual) + + defer func() { + // delete buffer from the global map to prevent a memory leak + value, loaded := sinkMap.LoadAndDelete(testKey) + require.True(overrides.t, loaded) + require.Equal(overrides.t, sink, value) + }() + } + if overrides.f != nil { + f = overrides.f + } + if overrides.opts != nil { + opts = overrides.opts + } + } + + // when using the trace or all log levels, an error log will contain the full stack. + // this is too noisy for regular use because things like leader election conflicts + // result in transient errors and we do not want all of that noise in the logs. + // this check is performed dynamically on the global log level. + return newZapr(globalLevel, LevelTrace, encoding, path, f, opts...) +} + +func newZapr(level zap.AtomicLevel, addStack zapcore.LevelEnabler, encoding, path string, f func(config *zap.Config), opts ...zap.Option) (logr.Logger, func(), error) { + opts = append([]zap.Option{zap.AddStacktrace(addStack)}, opts...) + + config := zap.Config{ + Level: level, + Development: false, + DisableCaller: false, + DisableStacktrace: true, // handled via the AddStacktrace call above + Sampling: nil, // keep all logs for now + Encoding: encoding, + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "timestamp", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, // included in caller + StacktraceKey: "stacktrace", + SkipLineEnding: false, + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: levelEncoder, + // human-readable and machine parsable with microsecond precision (same as klog, kube audit event, etc) + EncodeTime: zapcore.TimeEncoderOfLayout(metav1.RFC3339Micro), + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: callerEncoder, + EncodeName: nil, + NewReflectedEncoder: nil, + ConsoleSeparator: " ", + }, + OutputPaths: []string{path}, + ErrorOutputPaths: []string{path}, + InitialFields: nil, + } + + f(&config) + + log, err := config.Build(opts...) + if err != nil { + return logr.Logger{}, nil, fmt.Errorf("failed to build zap logger: %w", err) + } + + return zapr.NewLogger(log), func() { _ = log.Sync() }, nil +} + +func levelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + plogLevel := zapLevelToPlogLevel(l) + + if len(plogLevel) == 0 { + return // this tells zap that it should handle encoding the level itself because we do not know the mapping + } + + enc.AppendString(string(plogLevel)) +} + +func zapLevelToPlogLevel(l zapcore.Level) LogLevel { + if l > 0 { + // best effort mapping, the zap levels do not really translate to klog + // but this is correct for "error" level which is all we need for logr + return LogLevel(l.String()) + } + + // klog levels are inverted when zap handles them + switch { + case -l >= klogLevelAll: + return LevelAll + case -l >= KlogLevelTrace: + return LevelTrace + case -l >= KlogLevelDebug: + return LevelDebug + case -l >= KlogLevelInfo: + return LevelInfo + default: + return "" // warning is handled via a custom key since klog level 0 is ambiguous + } +} + +func callerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(caller.String() + funcEncoder(caller)) +} + +func funcEncoder(caller zapcore.EntryCaller) string { + funcName := caller.Function + if idx := strings.LastIndexByte(funcName, '/'); idx != -1 { + funcName = funcName[idx+1:] // keep everything after the last / + } + return "$" + funcName +} + +func humanDurationEncoder(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(duration.HumanDuration(d)) +} + +func humanTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Local().Format(time.RFC1123)) +} diff --git a/internal/registry/credentialrequest/rest_test.go b/internal/registry/credentialrequest/rest_test.go index f5b6f8cd..e7752e50 100644 --- a/internal/registry/credentialrequest/rest_test.go +++ b/internal/registry/credentialrequest/rest_test.go @@ -71,8 +71,8 @@ func TestCreate(t *testing.T) { it.Before(func() { r = require.New(t) ctrl = gomock.NewController(t) - logger = testutil.NewTranscriptLogger(t) - klog.SetLogger(logr.New(logger)) // this is unfortunately a global logger, so can't run these tests in parallel :( + logger = testutil.NewTranscriptLogger(t) // nolint: staticcheck // old test with lots of log statements + klog.SetLogger(logr.New(logger)) // this is unfortunately a global logger, so can't run these tests in parallel :( }) it.After(func() { diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 501cbd3f..772f0f5a 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -22,14 +22,12 @@ import ( "github.com/joshlf/go-acl" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apimachineryversion "k8s.io/apimachinery/pkg/version" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/pkg/version" "k8s.io/client-go/rest" - "k8s.io/component-base/logs" - "k8s.io/klog/v2" - "k8s.io/klog/v2/klogr" "k8s.io/utils/clock" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" @@ -272,7 +270,7 @@ func prepareControllers( pinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), secretInformer, - klogr.New(), + plog.Logr(), // nolint: staticcheck // old controller with lots of log statements controllerlib.WithInformer, ), singletonWorker). @@ -315,7 +313,7 @@ func startControllers(ctx context.Context, shutdown *sync.WaitGroup, buildContro } //nolint:funlen -func runSupervisor(podInfo *downward.PodInfo, cfg *supervisor.Config) error { +func runSupervisor(ctx context.Context, podInfo *downward.PodInfo, cfg *supervisor.Config) error { serverInstallationNamespace := podInfo.Namespace dref, supervisorDeployment, supervisorPod, err := deploymentref.New(podInfo) @@ -389,7 +387,6 @@ func runSupervisor(podInfo *downward.PodInfo, cfg *supervisor.Config) error { leaderElector, ) - ctx := signalCtx() shutdown := &sync.WaitGroup{} if err := startControllers(ctx, shutdown, buildControllersFunc); err != nil { @@ -504,12 +501,14 @@ func maybeSetupUnixPerms(endpoint *supervisor.Endpoint, pod *corev1.Pod) func() } } -func main() error { // return an error instead of klog.Fatal to allow defer statements to run - logs.InitLogs() - defer logs.FlushLogs() +func main() error { // return an error instead of plog.Fatal to allow defer statements to run + defer plog.Setup()() - klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get()) - klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2]) + plog.Always("Running supervisor", + "user-agent", rest.DefaultKubernetesUserAgent(), + "version", versionInfo(version.Get()), + "arguments", os.Args, + ) // Discover in which namespace we are installed. podInfo, err := downward.Load(os.Args[1]) @@ -517,17 +516,21 @@ func main() error { // return an error instead of klog.Fatal to allow defer stat return fmt.Errorf("could not read pod metadata: %w", err) } + ctx := signalCtx() + // Read the server config file. - cfg, err := supervisor.FromPath(os.Args[2]) + cfg, err := supervisor.FromPath(ctx, os.Args[2]) if err != nil { return fmt.Errorf("could not load config: %w", err) } - return runSupervisor(podInfo, cfg) + return runSupervisor(ctx, podInfo, cfg) } func Main() { if err := main(); err != nil { - klog.Fatal(err) + plog.Fatal(err) } } + +type versionInfo apimachineryversion.Info // hide .String() method from plog diff --git a/internal/testutil/testlogger/testlogger.go b/internal/testutil/testlogger/testlogger.go index 907d3722..17d31df5 100644 --- a/internal/testutil/testlogger/testlogger.go +++ b/internal/testutil/testlogger/testlogger.go @@ -24,14 +24,14 @@ type Logger struct { buffer syncBuffer } -// New returns a new test Logger. Use this for all new tests. +// Deprecated: Use plog.TestLogger or plog.TestZapr instead. This is meant for old tests only. func New(t *testing.T) *Logger { res := Logger{t: t} res.Logger = stdr.New(log.New(&res.buffer, "", 0)) return &res } -// Deprecated: NewLegacy returns a new test Logger. Use this for old tests if necessary. +// Deprecated: Use plog.TestLogger or plog.TestZapr instead. This is meant for old tests only. func NewLegacy(t *testing.T) *Logger { res := New(t) res.Logger = newStdLogger(log.New(&res.buffer, "", 0)) diff --git a/internal/testutil/transcript_logger.go b/internal/testutil/transcript_logger.go index add67486..f11da06a 100644 --- a/internal/testutil/transcript_logger.go +++ b/internal/testutil/transcript_logger.go @@ -24,6 +24,7 @@ type TranscriptLogMessage struct { Message string } +// Deprecated: Use plog.TestLogger or plog.TestZapr instead. This is meant for old tests only. func NewTranscriptLogger(t *testing.T) *TranscriptLogger { return &TranscriptLogger{t: t} } diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index 6c71b83a..cb480f8b 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -411,13 +411,13 @@ func (p *ProviderConfig) maybeFetchUserInfo(ctx context.Context, tok *oauth2.Tok func maybeLogClaims(msg, name string, claims map[string]interface{}) { if plog.Enabled(plog.LevelAll) { // log keys and values at all level data, _ := json.Marshal(claims) // nothing we can do if it fails, but it really never should - plog.Info(msg, "providerName", name, "claims", string(data)) + plog.All(msg, "providerName", name, "claims", string(data)) return } if plog.Enabled(plog.LevelDebug) { // log keys at debug level keys := sets.StringKeySet(claims).List() // note: this is only safe because the compiler asserts that claims is a map[string] - plog.Info(msg, "providerName", name, "keys", keys) + plog.Debug(msg, "providerName", name, "keys", keys) return } } diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 39f375d3..3ff1b9a4 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -33,6 +33,7 @@ import ( "go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -63,8 +64,6 @@ const ( defaultPasswordEnvVarName = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential httpLocationHeaderName = "Location" - - debugLogLevel = 4 ) // stdin returns the file descriptor for stdin as an int. @@ -356,7 +355,7 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { // If the ID token is still valid for a bit, return it immediately and skip the rest of the flow. cached := h.cache.GetToken(cacheKey) if cached != nil && cached.IDToken != nil && time.Until(cached.IDToken.Expiry.Time) > minIDTokenValidity { - h.logger.V(debugLogLevel).Info("Pinniped: Found unexpired cached token.") + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Found unexpired cached token.") return cached, nil } @@ -520,7 +519,7 @@ func (h *handlerState) getUsernameAndPassword() (string, string, error) { return "", "", fmt.Errorf("error prompting for username: %w", err) } } else { - h.logger.V(debugLogLevel).Info("Pinniped: Read username from environment variable", "name", defaultUsernameEnvVarName) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Read username from environment variable", "name", defaultUsernameEnvVarName) } password := h.getEnv(defaultPasswordEnvVarName) @@ -530,7 +529,7 @@ func (h *handlerState) getUsernameAndPassword() (string, string, error) { return "", "", fmt.Errorf("error prompting for password: %w", err) } } else { - h.logger.V(debugLogLevel).Info("Pinniped: Read password from environment variable", "name", defaultPasswordEnvVarName) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Read password from environment variable", "name", defaultPasswordEnvVarName) } return username, password, nil @@ -542,7 +541,7 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp // Attempt to open a local TCP listener, logging but otherwise ignoring any error. listener, err := h.listen("tcp", h.listenAddr) if err != nil { - h.logger.V(debugLogLevel).Error(err, "could not open callback listener") + h.logger.V(plog.KlogLevelDebug).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, @@ -578,7 +577,7 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp // 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") + h.logger.V(plog.KlogLevelDebug).Error(err, "could not open browser") } // Prompt the user to visit the authorize URL, and to paste a manually-copied auth code (if possible). @@ -709,7 +708,7 @@ func (h *handlerState) initOIDCDiscovery() error { return err } - h.logger.V(debugLogLevel).Info("Pinniped: Performing OIDC discovery", "issuer", h.issuer) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Performing OIDC discovery", "issuer", h.issuer) var err error h.provider, err = oidc.NewProvider(h.ctx, h.issuer) if err != nil { @@ -767,7 +766,7 @@ func stringSliceContains(slice []string, s string) bool { } func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidctypes.Token, error) { - h.logger.V(debugLogLevel).Info("Pinniped: Performing RFC8693 token exchange", "requestedAudience", h.requestedAudience) + h.logger.V(plog.KlogLevelDebug).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. if err := h.initOIDCDiscovery(); err != nil { return nil, err @@ -838,13 +837,13 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty } func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) { - h.logger.V(debugLogLevel).Info("Pinniped: Refreshing cached token.") + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Refreshing cached token.") upstreamOIDCIdentityProvider := h.getProvider(h.oauth2Config, h.provider, h.httpClient) refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token) if err != nil { // Ignore errors during refresh, but return nil which will trigger the full login flow. - h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.", "error", err.Error()) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Refresh failed.", "error", err.Error()) return nil, nil } @@ -865,7 +864,7 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req if h.useFormPost { // nolint:nestif // Return HTTP 405 for anything that's not a POST or an OPTIONS request. if r.Method != http.MethodPost && r.Method != http.MethodOptions { - h.logger.V(debugLogLevel).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method) w.WriteHeader(http.StatusMethodNotAllowed) return nil // keep listening for more requests } @@ -883,11 +882,11 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req origin := r.Header.Get("Origin") if origin == "" { // The CORS preflight request should have an origin. - h.logger.V(debugLogLevel).Info("Pinniped: Got OPTIONS request without origin header") + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got OPTIONS request without origin header") w.WriteHeader(http.StatusBadRequest) return nil // keep listening for more requests } - h.logger.V(debugLogLevel).Info("Pinniped: Got CORS preflight request from browser", "origin", origin) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got CORS preflight request from browser", "origin", origin) // To tell the browser that it is okay to make the real POST request, return the following response. w.Header().Set("Access-Control-Allow-Origin", allowOrigin) w.Header().Set("Vary", "*") // supposed to use Vary when Access-Control-Allow-Origin is a specific host @@ -921,7 +920,7 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req } else { // Return HTTP 405 for anything that's not a GET. if r.Method != http.MethodGet { - h.logger.V(debugLogLevel).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method) + h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method) w.WriteHeader(http.StatusMethodNotAllowed) return nil // keep listening for more requests } diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index 5a3c176a..2d8b266e 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -20,10 +20,6 @@ import ( "testing" "time" - "go.pinniped.dev/internal/net/phttp" - - "go.pinniped.dev/internal/testutil/tlsserver" - "github.com/coreos/go-oidc/v3/oidc" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -35,9 +31,12 @@ import ( "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/roundtripper" "go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider" + "go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/testlogger" + "go.pinniped.dev/internal/testutil/tlsserver" "go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -1891,7 +1890,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements + testLogger := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements klog.SetLogger(testLogger.Logger) tok, err := Login(tt.issuer, tt.clientID, @@ -2334,7 +2333,7 @@ func TestHandleAuthCodeCallback(t *testing.T) { state: state.State("test-state"), pkce: pkce.Code("test-pkce"), nonce: nonce.Nonce("test-nonce"), - logger: testlogger.New(t).Logger, + logger: plog.Logr(), // nolint: staticcheck // old test with no log assertions issuer: "https://valid-issuer.com/with/some/path", } if tt.opt != nil { From b99c4773a2df7fc8a2fed9a2e4fc72c86fddf211 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 2 Jun 2022 09:23:34 -0700 Subject: [PATCH 60/77] Use CSP headers in auth handler response When response_mode=form_post is requested, some error cases will be returned to the client using the form_post web page to POST the result back to the client's redirect URL. --- internal/oidc/auth/auth_handler.go | 10 ++++++++-- internal/oidc/auth/auth_handler_test.go | 21 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index b4b9fccd..698ea7f3 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -24,6 +24,7 @@ import ( "go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/login" "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -46,7 +47,7 @@ func NewHandler( upstreamStateEncoder oidc.Encoder, cookieCodec oidc.Codec, ) 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 { if r.Method != http.MethodPost && r.Method != http.MethodGet { // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Authorization Servers MUST support the use of the HTTP GET and POST methods defined in @@ -105,7 +106,12 @@ func NewHandler( upstreamStateEncoder, cookieCodec, ) - })) + }) + + // During a response_mode=form_post auth request using the browser flow, the custom form_post html page may + // be used to post certain errors back to the CLI from this handler's response, so allow the form_post + // page's CSS and JS to run. + return securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy()) } func handleAuthRequestForLDAPUpstreamCLIFlow( diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index dc93c42a..11431a0b 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -521,6 +521,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus int wantContentType string wantBodyString string + wantBodyRegex string wantBodyJSON string wantCSRFValueInCookieHeader string wantBodyStringWithLocationInHref bool @@ -1537,6 +1538,20 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, + { + name: "form_post page is used to send errors to client using OIDC upstream browser flow with response_mode=form_post", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_mode": "form_post", "scope": "openid profile email tuna"}), + wantStatus: http.StatusOK, + wantContentType: htmlContentType, + wantBodyRegex: ` Date: Thu, 2 Jun 2022 10:30:03 -0700 Subject: [PATCH 61/77] Allow PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW env var to override flow Env var may be used with CLI to override the flow selected by the --upstream-identity-provider-flow CLI flag. --- cmd/pinniped/cmd/login_oidc.go | 36 +++++-- cmd/pinniped/cmd/login_oidc_test.go | 149 ++++++++++++++++++++++++++-- 2 files changed, 172 insertions(+), 13 deletions(-) diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 8f9378f5..b31f8dd6 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -32,6 +32,15 @@ import ( "go.pinniped.dev/pkg/oidcclient/oidctypes" ) +const ( + // The user may override the flow selection made by `--upstream-identity-provider-flow` using an env var. + // This allows the user to override their default flow selected inside their Pinniped-compatible kubeconfig file. + // A user might want to use this env var, for example, to choose the "browser_authcode" flow when using a kubeconfig + // which specifies "cli_password" when using an IDE plugin where there is no interactive CLI available. This allows + // the user to use one kubeconfig file for both flows. + upstreamIdentityProviderFlowEnvVarName = "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW" +) + // nolint: gochecknoinits func init() { loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps())) @@ -165,6 +174,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin flowOpts, err := flowOptions( idpdiscoveryv1alpha1.IDPType(flags.upstreamIdentityProviderType), idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow), + deps, ) if err != nil { return err @@ -250,9 +260,21 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) } -func flowOptions(requestedIDPType idpdiscoveryv1alpha1.IDPType, requestedFlow idpdiscoveryv1alpha1.IDPFlow) ([]oidcclient.Option, error) { +func flowOptions( + requestedIDPType idpdiscoveryv1alpha1.IDPType, + requestedFlow idpdiscoveryv1alpha1.IDPFlow, + deps oidcLoginCommandDeps, +) ([]oidcclient.Option, error) { useCLIFlow := []oidcclient.Option{oidcclient.WithCLISendingCredentials()} + // If the env var is set to override the --upstream-identity-provider-type flag, then override it. + flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName) + flowSource := "--upstream-identity-provider-flow" + if hasFlowOverride { + requestedFlow = idpdiscoveryv1alpha1.IDPFlow(flowOverride) + flowSource = upstreamIdentityProviderFlowEnvVarName + } + switch requestedIDPType { case idpdiscoveryv1alpha1.IDPTypeOIDC: switch requestedFlow { @@ -262,19 +284,21 @@ func flowOptions(requestedIDPType idpdiscoveryv1alpha1.IDPType, requestedFlow id return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here default: return nil, fmt.Errorf( - "--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)", - requestedIDPType, requestedFlow, strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String(), idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}, ", ")) + "%s value not recognized for identity provider type %q: %s (supported values: %s)", + flowSource, requestedIDPType, requestedFlow, + strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String(), idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}, ", ")) } case idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory: switch requestedFlow { case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "": return useCLIFlow, nil case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode: - return nil, nil + return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here default: return nil, fmt.Errorf( - "--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)", - requestedIDPType, requestedFlow, strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", ")) + "%s value not recognized for identity provider type %q: %s (supported values: %s)", + flowSource, requestedIDPType, requestedFlow, + strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", ")) } default: // Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236 diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 492891e2..2e4fbd45 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -148,7 +148,7 @@ func TestLoginOIDCCommand(t *testing.T) { `), }, { - name: "invalid upstream type", + name: "invalid upstream type is an error", args: []string{ "--issuer", "test-issuer", "--upstream-identity-provider-type", "invalid", @@ -158,6 +158,18 @@ func TestLoginOIDCCommand(t *testing.T) { Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory) `), }, + { + name: "invalid upstream type when flow override env var is used is still an error", + args: []string{ + "--issuer", "test-issuer", + "--upstream-identity-provider-type", "invalid", + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"}, + wantError: true, + wantStderr: here.Doc(` + Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory) + `), + }, { name: "oidc upstream type with default flow is allowed", args: []string{ @@ -193,6 +205,32 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 4, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, + { + name: "oidc upstream type with CLI flow in flow override env var is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "oidc", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"}, + wantOptionsCount: 5, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, + { + name: "oidc upstream type with with browser flow in flow override env var is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "oidc", + "--upstream-identity-provider-flow", "cli_password", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"}, + wantOptionsCount: 4, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, { name: "oidc upstream type with unsupported flow is an error", args: []string{ @@ -207,6 +245,21 @@ func TestLoginOIDCCommand(t *testing.T) { Error: --upstream-identity-provider-flow value not recognized for identity provider type "oidc": foobar (supported values: browser_authcode, cli_password) `), }, + { + name: "oidc upstream type with unsupported flow in flow override env var is an error", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "oidc", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"}, + wantError: true, + wantStderr: here.Doc(` + Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "oidc": foo (supported values: browser_authcode, cli_password) + `), + }, { name: "ldap upstream type with default flow is allowed", args: []string{ @@ -253,6 +306,32 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 4, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, + { + name: "ldap upstream type with CLI flow in flow override env var is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "ldap", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"}, + wantOptionsCount: 5, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, + { + name: "ldap upstream type with browser_authcode flow in flow override env var is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "ldap", + "--upstream-identity-provider-flow", "cli_password", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"}, + wantOptionsCount: 4, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, { name: "ldap upstream type with unsupported flow is an error", args: []string{ @@ -267,6 +346,21 @@ func TestLoginOIDCCommand(t *testing.T) { Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode) `), }, + { + name: "ldap upstream type with unsupported flow in flow override env var is an error", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "ldap", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"}, + wantError: true, + wantStderr: here.Doc(` + Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode) + `), + }, { name: "active directory upstream type with CLI flow is allowed", args: []string{ @@ -291,6 +385,32 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 4, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, + { + name: "active directory upstream type with CLI flow in flow override env var is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "activedirectory", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"}, + wantOptionsCount: 5, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, + { + name: "active directory upstream type with browser_authcode in flow override env var is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "activedirectory", + "--upstream-identity-provider-flow", "cli_password", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"}, + wantOptionsCount: 4, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, { name: "active directory upstream type with unsupported flow is an error", args: []string{ @@ -305,6 +425,21 @@ func TestLoginOIDCCommand(t *testing.T) { Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode) `), }, + { + name: "active directory upstream type with unsupported flow in flow override env var is an error", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "activedirectory", + "--upstream-identity-provider-flow", "browser_authcode", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"}, + wantError: true, + wantStderr: here.Doc(` + Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode) + `), + }, { name: "login error", args: []string{ @@ -348,8 +483,8 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 4, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", wantLogs: []string{ - nowStr + ` pinniped-login cmd/login_oidc.go:222 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, - nowStr + ` pinniped-login cmd/login_oidc.go:242 No concierge configured, skipping token credential exchange`, + nowStr + ` pinniped-login cmd/login_oidc.go:232 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:252 No concierge configured, skipping token credential exchange`, }, }, { @@ -378,10 +513,10 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 11, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n", wantLogs: []string{ - nowStr + ` pinniped-login cmd/login_oidc.go:222 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, - nowStr + ` pinniped-login cmd/login_oidc.go:232 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, - nowStr + ` pinniped-login cmd/login_oidc.go:240 Successfully exchanged token for cluster credential.`, - nowStr + ` pinniped-login cmd/login_oidc.go:247 caching cluster credential for future use.`, + nowStr + ` pinniped-login cmd/login_oidc.go:232 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:242 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:250 Successfully exchanged token for cluster credential.`, + nowStr + ` pinniped-login cmd/login_oidc.go:257 caching cluster credential for future use.`, }, }, } From cb8685b9427399d5625ea0d1870e537fa1cbd1ed Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 2 Jun 2022 11:27:54 -0700 Subject: [PATCH 62/77] Add e2e test for PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW env var --- test/integration/e2e_test.go | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 0e16bde2..1c07b48e 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -53,6 +53,15 @@ import ( func TestE2EFullIntegration_Browser(t *testing.T) { env := testlib.IntegrationEnv(t) + // Avoid allowing PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW to interfere with these tests. + originalFlowEnvVarValue, flowOverrideEnvVarSet := os.LookupEnv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW") + if flowOverrideEnvVarSet { + require.NoError(t, os.Unsetenv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")) + t.Cleanup(func() { + require.NoError(t, os.Setenv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW", originalFlowEnvVarValue)) + }) + } + topSetupCtx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute) defer cancelFunc() @@ -1015,6 +1024,66 @@ func TestE2EFullIntegration_Browser(t *testing.T) { expectedGroups, ) }) + + // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow. + t.Run("with Supervisor LDAP upstream IDP and browser flow selected by env var override with with form_post automatic authcode delivery to CLI", func(t *testing.T) { + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests + + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. + page := browsertest.Open(t) + + expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs + + setupClusterForEndToEndLDAPTest(t, expectedUsername, env) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/test-sessions.yaml" + + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-skip-browser", + "--oidc-ca-bundle", testCABundlePath, + "--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var + "--oidc-session-cache", sessionCachePath, + }) + + // Override the --upstream-identity-provider-flow flag from the kubeconfig using the env var. + require.NoError(t, os.Setenv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW", "browser_authcode")) + t.Cleanup(func() { + require.NoError(t, os.Unsetenv("PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")) + }) + + // Run "kubectl get namespaces" which should trigger a browser login via the plugin. + kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + + // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, page) + + // Confirm that we got to the Supervisor's login page, fill out the form, and submit the form. + browsertest.LoginToUpstreamLDAP(t, page, downstream.Spec.Issuer, + expectedUsername, env.SupervisorUpstreamLDAP.TestUserPassword) + + formpostExpectSuccessState(t, page) + + requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) + }) } func startKubectlAndOpenAuthorizationURLInBrowser(testCtx context.Context, t *testing.T, kubectlCmd *exec.Cmd, page *agouti.Page) chan string { From fd9d641b5c2096ed05a1dd6ae98f9d2e5e092bc0 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 6 Jun 2022 09:47:50 -0700 Subject: [PATCH 63/77] Add doc for PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW env var --- site/content/docs/howto/login.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/site/content/docs/howto/login.md b/site/content/docs/howto/login.md index b2ae46a0..61034590 100644 --- a/site/content/docs/howto/login.md +++ b/site/content/docs/howto/login.md @@ -125,6 +125,11 @@ will depend on which type of identity provider was configured. Unlike the optional flow for OIDC providers described above, this optional flow does not need to be configured in the LDAPIdentityProvider or ActiveDirectoryIdentityProvider resource, so it is always available for end-users. +The flow selected by the `--upstream-identity-provider-flow` CLI flag may be overridden by using the +`PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW` environment variable for the CLI at runtime. This environment variable +may be set to the same values as the CLI flag (`browser_authcode` or `cli_password`). This allows a user to switch +flows based on their needs without editing their kubeconfig file. + 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. From a3ec15862d65a091ee813923f0f7350e052fd47b Mon Sep 17 00:00:00 2001 From: Mo Khan Date: Mon, 6 Jun 2022 16:41:38 -0400 Subject: [PATCH 64/77] Run CodeQL on dynamic_clients branch --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6ae22f29..800b8a1e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,10 +2,10 @@ name: "CodeQL" on: push: - branches: [ main, release* ] + branches: [ main, release*, dynamic_clients ] pull_request: # The branches below must be a subset of the branches above - branches: [ main, release* ] + branches: [ main, release*, dynamic_clients ] schedule: - cron: '39 13 * * 2' From 52bbbcf7e81f528fc269ae25e3116ab8c1f481fc Mon Sep 17 00:00:00 2001 From: Anjali Telang Date: Fri, 3 Jun 2022 10:59:51 -0400 Subject: [PATCH 65/77] margo's suggestions --- site/content/docs/img/ldap-ad-ui.png | Bin 0 -> 174326 bytes .../posts/2022-06-01-json-logging-ldap-ui.md | 91 ++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 site/content/docs/img/ldap-ad-ui.png create mode 100644 site/content/posts/2022-06-01-json-logging-ldap-ui.md diff --git a/site/content/docs/img/ldap-ad-ui.png b/site/content/docs/img/ldap-ad-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..9b2610db665fb654cc75502966145b9dd8be9fec GIT binary patch literal 174326 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 literal 0 HcmV?d00001 diff --git a/site/content/posts/2022-06-01-json-logging-ldap-ui.md b/site/content/posts/2022-06-01-json-logging-ldap-ui.md new file mode 100644 index 00000000..02e0c07b --- /dev/null +++ b/site/content/posts/2022-06-01-json-logging-ldap-ui.md @@ -0,0 +1,91 @@ +--- +title: "Pinniped v0.18.0: With User-Friendly features such as JSON formatted logs, LDAP/ActiveDirectory UI Support" +slug: formatted-logs-ui-based-ldap-logins +date: 2022-06-01 +author: Anjali Telang +image: https://images.unsplash.com/photo-1587738972117-c12f8389f1d4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1752&q=80 +excerpt: "With v0.18.0 you get cool new features like nicely formatted JSON logs, UI to login to your LDAP or ActiveDirectory Identity Provider, and more" +tags: ['Margo Crawford','Ryan Richard', 'Mo Khan', 'Anjali Telang', 'release'] +--- + +![Friendly seal](https://images.unsplash.com/photo-1587738972117-c12f8389f1d4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1752&q=80) +*Photo by [Steve Adams](https://unsplash.com/@sradams57) on [Unsplash](https://unsplash.com/s/photos/seal)* + +We've listened to your requests and are excited to bring some cool user-friendly features that will enhance your Kubernetes Authentication experience. From this release onwards, we will have Pinniped logs in JSON format. We also bring you the ability to use a User Interface (UI) to login with your LDAP or ActiveDirectory credentials. + +## JSON Formatted logs + +[Kubernetes 1.19](https://kubernetes.io/blog/2020/09/04/kubernetes-1-19-introducing-structured-logs/) introduced the ability to have logs emitted in JSON log format. There are many benefits to using JSON as listed in this [KEP for structured logging](https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/1602-structured-logging#json-output-format) : + +* Broadly adopted by logging libraries with very efficient implementations (zap, zerolog). +* Out of the box support by many logging backends (Elasticsearch, Stackdriver, BigQuery, Splunk) +* Easily parsable and transformable +* Existing tools for ad-hoc analysis (jq) + +Considering these benefits, from this release onwards we will *default to JSON formatted logs* for the Pinniped Supervisor and Concierge components. + +**Users please note: We are deprecating the text based log format with this release and we will remove the configuration in a future release.** + +We realize that it may take time for some users to adapt to this new format especially if they have existing tooling around *text* based logs. With that in mind, we will allow users to deploy Pinniped Supervisor or Concierge by setting *deprecated_log_format:text* in the values.yaml deployment file for these components. However, Please consider moving to json formatted logs for compatibility with latest releases. We will announce 2 releases prior to removing the field, as is our policy for removing configurations. + +With this release, the Pinniped CLI logs have also been enhanced to emit useful information such as timestamps and line numbers of files, as shown in the example below: + +``` +$ pinniped get kubeconfig +Tue, 24 May 2022 11:18:50 EDT cmd/kubeconfig.go:584 discovered CredentialIssuer {"name": "concierge-config"} +Tue, 24 May 2022 11:18:50 EDT cmd/kubeconfig.go:419 discovered Concierge operating in impersonation proxy mode +Tue, 24 May 2022 11:18:50 EDT cmd/kubeconfig.go:432 discovered Concierge endpoint {"endpoint": "https://abcd"} +Tue, 24 May 2022 11:18:50 EDT cmd/kubeconfig.go:447 discovered Concierge certificate authority bundle {"roots": 1} +Tue, 24 May 2022 11:18:50 EDT cmd/kubeconfig.go:469 discovered WebhookAuthenticator {"name": "wa"} +``` + + +## LDAP User Interface + +With [v0.18.0](https://github.com/vmware-tanzu/pinniped/releases/tag/v0.18.0) we are also adding support for a User Interface that users can access to input their credentials and login to their LDAP or Active Directory Identity Provider(IDP). This feature is a first step in our effort to provide a UI-driven workflow for our users. We will have more features that support UI workflows coming up in our next release, so stay tuned for that! + +When using the Pinniped CLI, a successful login takes the user to the regular form_post success page, just like the previously supported *browser authcode flow* for an OIDC IDP. +This feature changes how the `pinniped get kubeconfig` cli deals with ambiguous flows. Previously, if there was more than one flow advertised for an IDP, the cli would require users to use the `--upstream-identity-provider-flow` flag. Now, it chooses the first flow type in the Supervisor's discovery response by default. + +Here's a snapshot of the UI hosted by the Pinniped Supervisor: +![LDAP / ActiveDirectory UI for login ](/docs/img/ldap-ad-ui.png) + +### Features included in the UI +The UI will provide the following features for now: + +* A username and password fields along with the submit button +* Generalized error messaging for failed logins that do not expose sensitive information +* Provides information easily allowing a user to identify the screen as belonging to Pinniped with the name of their Identity Provider displayed on the page +* Addresses basic security concerns for web forms such as use of HTTPS, a password field, CSRF protection and redirect protection +* Prevents LDAP injection attacks +* Screens that are accessible and friendly to screen readers +* Screens that are friendly to password managers + +### Future enhancements for the UI +This is our effort to provide a basic UI for logins. We will enhance the UI further in future releases based on the feedback we receive from our users. +This can include (but is not limited to) the following features: + +* Advanced UI features such as remember me and reveal password +* Branding & customization, besides what we provide today with the basic Pinniped information. +* Support for SSO integrations +* Internationalization or localization. Note that the CLI doesn't currently support this either. + +*Note* that Pinniped relies on the user's IDP to address advanced security concerns such as brute force protection, username enumeration, etc. + +## What else is in this release? +Refer to the [release notes for v0.18.0](https://github.com/vmware-tanzu/pinniped/releases/tag/v0.18.0) for a complete list of fixes and features included in the release. + +## Community contributors + +The Pinniped community continues to grow, and is a vital part of the project's success. + +[Are you using Pinniped?](https://github.com/vmware-tanzu/pinniped/discussions/152) +Have you tried any of our new features? Let us know what you think of our logging and UI features and if you are looking for any of the enhancements mentioned above. + +We thrive on community feedback and would like to hear more! + +Reach out to us in [#pinniped](https://kubernetes.slack.com/archives/C01BW364RJA) on Kubernetes Slack, +[create an issue](https://github.com/vmware-tanzu/pinniped/issues/new/choose) on our Github repository, +or start a [discussion](https://github.com/vmware-tanzu/pinniped/discussions). + +{{< community >}} From 8170889aefc4756dcd131f6f76fc05ef829e2df0 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 7 Jun 2022 11:20:59 -0700 Subject: [PATCH 66/77] Update CSP header expectations in TestSupervisorLogin_Browser int test --- test/integration/supervisor_login_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 3713175a..3c2f045a 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -1809,7 +1809,15 @@ func doTokenExchange(t *testing.T, config *oauth2.Config, tokenResponse *oauth2. func expectSecurityHeaders(t *testing.T, response *http.Response, expectFositeToOverrideSome bool) { h := response.Header - assert.Equal(t, "default-src 'none'; frame-ancestors 'none'", h.Get("Content-Security-Policy")) + + cspHeader := h.Get("Content-Security-Policy") + require.Contains(t, cspHeader, "script-src '") // loose assertion + require.Contains(t, cspHeader, "style-src '") // loose assertion + require.Contains(t, cspHeader, "img-src data:") + require.Contains(t, cspHeader, "connect-src *") + require.Contains(t, cspHeader, "default-src 'none'") + require.Contains(t, cspHeader, "frame-ancestors 'none'") + assert.Equal(t, "DENY", h.Get("X-Frame-Options")) assert.Equal(t, "1; mode=block", h.Get("X-XSS-Protection")) assert.Equal(t, "nosniff", h.Get("X-Content-Type-Options")) From 7751c0bf5947e3a851da97056d469f6e0e994ec2 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 6 Jun 2022 14:37:22 -0700 Subject: [PATCH 67/77] Bump project deps, including kube 0.23.6->0.24.1 and Go 1.18.1->1.18.3 Several API changes in Kube required changes in Pinniped code. Signed-off-by: Monis Khan --- Dockerfile | 2 +- go.mod | 26 +++---- go.sum | 42 +++++++++++ hack/Dockerfile_fips | 2 +- .../webhookcachefiller/webhookcachefiller.go | 24 ++++++- .../kubecertagent/mocks/mockdynamiccert.go | 11 +-- internal/dynamiccert/provider.go | 7 +- internal/kubeclient/kubeclient_test.go | 72 +++++++++---------- .../formposthtml/formposthtml_test.go | 4 +- 9 files changed, 129 insertions(+), 61 deletions(-) diff --git a/Dockerfile b/Dockerfile index a5434d4f..b972bd5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -FROM golang:1.18.1 as build-env +FROM golang:1.18.3 as build-env WORKDIR /work COPY . . diff --git a/go.mod b/go.mod index 5ee1267e..3608bfcf 100644 --- a/go.mod +++ b/go.mod @@ -38,10 +38,10 @@ replace ( require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 - github.com/coreos/go-oidc/v3 v3.1.0 + github.com/coreos/go-oidc/v3 v3.2.0 github.com/creack/pty v1.1.18 github.com/davecgh/go-spew v1.1.1 - github.com/felixge/httpsnoop v1.0.2 + github.com/felixge/httpsnoop v1.0.3 github.com/go-ldap/ldap/v3 v3.4.3 github.com/go-logr/logr v1.2.3 github.com/go-logr/stdr v1.2.2 @@ -63,7 +63,7 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.1 - github.com/tdewolff/minify/v2 v2.11.2 + github.com/tdewolff/minify/v2 v2.11.9 go.uber.org/atomic v1.9.0 go.uber.org/zap v1.21.0 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f @@ -73,15 +73,15 @@ require ( golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 golang.org/x/text v0.3.7 gopkg.in/square/go-jose.v2 v2.6.0 - k8s.io/api v0.23.6 - k8s.io/apiextensions-apiserver v0.23.6 - k8s.io/apimachinery v0.23.6 - k8s.io/apiserver v0.23.6 - k8s.io/client-go v0.23.6 - k8s.io/component-base v0.23.6 + k8s.io/api v0.24.1 + k8s.io/apiextensions-apiserver v0.24.1 + k8s.io/apimachinery v0.24.1 + k8s.io/apiserver v0.24.1 + k8s.io/client-go v0.24.1 + k8s.io/component-base v0.24.1 k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 k8s.io/klog/v2 v2.60.1 - k8s.io/kube-aggregator v0.23.6 + k8s.io/kube-aggregator v0.24.1 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/yaml v1.3.0 ) @@ -99,6 +99,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/coreos/go-oidc v2.2.1+incompatible // indirect github.com/coreos/go-semver v0.3.0 // indirect @@ -119,6 +120,7 @@ require ( github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -154,7 +156,7 @@ require ( github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/tdewolff/parse/v2 v2.5.29 // indirect + github.com/tdewolff/parse/v2 v2.5.33 // indirect go.etcd.io/etcd/api/v3 v3.5.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect go.etcd.io/etcd/client/v3 v3.5.4 // indirect @@ -184,7 +186,7 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect + k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect diff --git a/go.sum b/go.sum index 86bc0ea4..75df4088 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= @@ -134,6 +135,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -183,6 +186,8 @@ github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjs github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw= github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= +github.com/coreos/go-oidc/v3 v3.2.0 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc= +github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -258,6 +263,8 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -682,7 +689,10 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= +github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -986,6 +996,7 @@ 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= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1269,8 +1280,12 @@ 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.11.2 h1:PpaPWhNlMVjkAKaOj0bbPv6KCVnrm8jbVwG7OtSdAqw= github.com/tdewolff/minify/v2 v2.11.2/go.mod h1:NxozhBtgUVypPLzQdV96wkIu9J9vAiVmBcKhfC2zMfg= +github.com/tdewolff/minify/v2 v2.11.9 h1:1q5728c0QICKlp2X1n7OiaiiFFzCzsq7uxAkv+eykT8= +github.com/tdewolff/minify/v2 v2.11.9/go.mod h1:XHKhaRF/vTa3EP4JX8oZ2CO4crGEtVOiSoqUED953wM= github.com/tdewolff/parse/v2 v2.5.29 h1:Uf0OtZL9YaUXTuHEOitdo9lD90P0XTwCjZi+KbGChuM= github.com/tdewolff/parse/v2 v2.5.29/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/parse/v2 v2.5.33 h1:D75KlhAeCSQg4Na8cWKehJdPJoZxwdpRbTZw7lZFWNQ= +github.com/tdewolff/parse/v2 v2.5.33/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= @@ -1310,6 +1325,7 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/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= @@ -1320,14 +1336,17 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.etcd.io/etcd/pkg/v3 v3.5.0 h1:ntrg6vvKRW26JRmHTE0iNlDgYK6JX3hg/4cD62X0ixk= @@ -1436,6 +1455,7 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1545,6 +1565,7 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1690,6 +1711,7 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1723,6 +1745,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1829,6 +1852,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1959,6 +1983,7 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= @@ -2090,17 +2115,30 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= k8s.io/api v0.23.6 h1:yOK34wbYECH4RsJbQ9sfkFK3O7f/DUHRlzFehkqZyVw= k8s.io/api v0.23.6/go.mod h1:1kFaYxGCFHYp3qd6a85DAj/yW8aVD6XLZMqJclkoi9g= +k8s.io/api v0.24.1 h1:BjCMRDcyEYz03joa3K1+rbshwh1Ay6oB53+iUx2H8UY= +k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= k8s.io/apiextensions-apiserver v0.23.6 h1:v58cQ6Z0/GK1IXYr+oW0fnYl52o9LTY0WgoWvI8uv5Q= k8s.io/apiextensions-apiserver v0.23.6/go.mod h1:YVh17Mphv183THQJA5spNFp9XfoidFyL3WoDgZxQIZU= +k8s.io/apiextensions-apiserver v0.24.1 h1:5yBh9+ueTq/kfnHQZa0MAo6uNcPrtxPMpNQgorBaKS0= +k8s.io/apiextensions-apiserver v0.24.1/go.mod h1:A6MHfaLDGfjOc/We2nM7uewD5Oa/FnEbZ6cD7g2ca4Q= k8s.io/apimachinery v0.23.6 h1:RH1UweWJkWNTlFx0D8uxOpaU1tjIOvVVWV/bu5b3/NQ= k8s.io/apimachinery v0.23.6/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/apimachinery v0.24.1 h1:ShD4aDxTQKN5zNf8K1RQ2u98ELLdIW7jEnlO9uAMX/I= +k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apiserver v0.23.6 h1:p94LiXcsSnpSDIl4cv98liBuFKcaygSCNopFNfMg/Ac= k8s.io/apiserver v0.23.6/go.mod h1:5PU32F82tfErXPmf7FXhd/UcuLfh97tGepjKUgJ2atg= +k8s.io/apiserver v0.24.1 h1:LAA5UpPOeaREEtFAQRUQOI3eE5So/j5J3zeQJjeLdz4= +k8s.io/apiserver v0.24.1/go.mod h1:dQWNMx15S8NqJMp0gpYfssyvhYnkilc1LpExd/dkLh0= k8s.io/client-go v0.23.6 h1:7h4SctDVQAQbkHQnR4Kzi7EyUyvla5G1pFWf4+Od7hQ= k8s.io/client-go v0.23.6/go.mod h1:Umt5icFOMLV/+qbtZ3PR0D+JA6lvvb3syzodv4irpK4= +k8s.io/client-go v0.24.1 h1:w1hNdI9PFrzu3OlovVeTnf4oHDt+FJLd9Ndluvnb42E= +k8s.io/client-go v0.24.1/go.mod h1:f1kIDqcEYmwXS/vTbbhopMUbhKp2JhOeVTfxgaCIlF8= k8s.io/code-generator v0.23.6/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= +k8s.io/code-generator v0.24.1/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.23.6 h1:8dhVZ4VrRcNdV2EGjl8tj8YOHwX6ysgCGMJ2Oyy0NW8= k8s.io/component-base v0.23.6/go.mod h1:FGMPeMrjYu0UZBSAFcfloVDplj9IvU+uRMTOdE23Fj0= +k8s.io/component-base v0.24.1 h1:APv6W/YmfOWZfo+XJ1mZwep/f7g7Tpwvdbo9CQLDuts= +k8s.io/component-base v0.24.1/go.mod h1:DW5vQGYVCog8WYpNob3PMmmsY8A3L9QZNg4j/dV3s38= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -2111,9 +2149,13 @@ k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-aggregator v0.23.6 h1:/p1FvmG3je8kSv+i6uJoK+LkViOgu1vhV+BpGgibdCk= k8s.io/kube-aggregator v0.23.6/go.mod h1:cubFdoSJRMEN+ilg1ErhNIoplJwyYbmgn3bUlen8KjA= +k8s.io/kube-aggregator v0.24.1 h1:OXnkMFY20gaVV4cwKSayOZobdETOvRhgDxCHxriBygU= +k8s.io/kube-aggregator v0.24.1/go.mod h1:vZvRALCO32hrIuREhkYwLq5Crc0zh6SxzJDAKrQM1+k= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf h1:M9XBsiMslw2lb2ZzglC0TOkBPK5NQi0/noUrdnoFwUg= k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= diff --git a/hack/Dockerfile_fips b/hack/Dockerfile_fips index c8017953..ce77f805 100644 --- a/hack/Dockerfile_fips +++ b/hack/Dockerfile_fips @@ -12,7 +12,7 @@ # any type of fips certification. # use go-boringcrypto rather than main go -FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.1b7 as build-env +FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.3b7 as build-env WORKDIR /work COPY . . diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go index d394c913..61354f61 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/net" "k8s.io/apiserver/pkg/authentication/authenticator" + webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -118,7 +119,28 @@ func newWebhookAuthenticator( // custom proxy stuff used by the API server. var customDial net.DialFunc + // TODO refactor this code to directly construct the rest.Config + // ideally we would keep rest config generation contained to the kubeclient package + // but this will require some form of a new WithTLSConfigFunc kubeclient.Option + // ex: + // _, caBundle, err := pinnipedauthenticator.CABundle(spec.TLS) + // ... + // restConfig := &rest.Config{ + // Host: spec.Endpoint, + // TLSClientConfig: rest.TLSClientConfig{CAData: caBundle}, + // // copied from k8s.io/apiserver/pkg/util/webhook + // Timeout: 30 * time.Second, + // QPS: -1, + // } + // client, err := kubeclient.New(kubeclient.WithConfig(restConfig), kubeclient.WithTLSConfigFunc(ptls.Default)) + // ... + // then use client.JSONConfig as clientConfig + clientConfig, err := webhookutil.LoadKubeconfig(temp.Name(), customDial) + if err != nil { + return nil, err + } + // this uses a http client that does not honor our TLS config // TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106155 - return webhook.New(temp.Name(), version, implicitAuds, *webhook.DefaultRetryBackoff(), customDial) + return webhook.New(clientConfig, version, implicitAuds, *webhook.DefaultRetryBackoff()) } diff --git a/internal/controller/kubecertagent/mocks/mockdynamiccert.go b/internal/controller/kubecertagent/mocks/mockdynamiccert.go index fda36b65..4fc00e1a 100644 --- a/internal/controller/kubecertagent/mocks/mockdynamiccert.go +++ b/internal/controller/kubecertagent/mocks/mockdynamiccert.go @@ -9,6 +9,7 @@ package mocks import ( + context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -80,7 +81,7 @@ func (mr *MockDynamicCertPrivateMockRecorder) Name() *gomock.Call { } // Run mocks base method. -func (m *MockDynamicCertPrivate) Run(arg0 int, arg1 <-chan struct{}) { +func (m *MockDynamicCertPrivate) Run(arg0 context.Context, arg1 int) { m.ctrl.T.Helper() m.ctrl.Call(m, "Run", arg0, arg1) } @@ -92,17 +93,17 @@ func (mr *MockDynamicCertPrivateMockRecorder) Run(arg0, arg1 interface{}) *gomoc } // RunOnce mocks base method. -func (m *MockDynamicCertPrivate) RunOnce() error { +func (m *MockDynamicCertPrivate) RunOnce(arg0 context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RunOnce") + ret := m.ctrl.Call(m, "RunOnce", arg0) ret0, _ := ret[0].(error) return ret0 } // RunOnce indicates an expected call of RunOnce. -func (mr *MockDynamicCertPrivateMockRecorder) RunOnce() *gomock.Call { +func (mr *MockDynamicCertPrivateMockRecorder) RunOnce(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOnce", reflect.TypeOf((*MockDynamicCertPrivate)(nil).RunOnce)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOnce", reflect.TypeOf((*MockDynamicCertPrivate)(nil).RunOnce), arg0) } // SetCertKeyContent mocks base method. diff --git a/internal/dynamiccert/provider.go b/internal/dynamiccert/provider.go index d5c76847..560dff55 100644 --- a/internal/dynamiccert/provider.go +++ b/internal/dynamiccert/provider.go @@ -1,9 +1,10 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package dynamiccert import ( + "context" "crypto/tls" "crypto/x509" "fmt" @@ -148,10 +149,10 @@ func (p *provider) AddListener(listener dynamiccertificates.Listener) { p.listeners = append(p.listeners, listener) } -func (p *provider) RunOnce() error { +func (p *provider) RunOnce(_ context.Context) error { return nil // no-op, but we want to make sure to stay in sync with dynamiccertificates.ControllerRunner } -func (p *provider) Run(workers int, stopCh <-chan struct{}) { +func (p *provider) Run(_ context.Context, workers int) { // no-op, but we want to make sure to stay in sync with dynamiccertificates.ControllerRunner } diff --git a/internal/kubeclient/kubeclient_test.go b/internal/kubeclient/kubeclient_test.go index 9b35c761..b3640a43 100644 --- a/internal/kubeclient/kubeclient_test.go +++ b/internal/kubeclient/kubeclient_test.go @@ -37,7 +37,7 @@ import ( ) const ( - someClusterName = "some cluster name" + someUID = "some fake UID" ) var ( @@ -115,13 +115,13 @@ func TestKubeclient(t *testing.T) { require.EqualError(t, err, `couldn't find object for path "/api/v1/namespaces/good-namespace/pods/this-pod-does-not-exist"`) // update - goodPodWithAnnotationsAndLabelsAndClusterName := with(goodPod, annotations(), labels(), clusterName()).(*corev1.Pod) + goodPodWithAnnotationsAndLabelsAndUID := with(goodPod, annotations(), labels(), uid()).(*corev1.Pod) pod, err = c.Kubernetes. CoreV1(). Pods(pod.Namespace). - Update(context.Background(), goodPodWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{}) + Update(context.Background(), goodPodWithAnnotationsAndLabelsAndUID, metav1.UpdateOptions{}) require.NoError(t, err) - require.Equal(t, goodPodWithAnnotationsAndLabelsAndClusterName, pod) + require.Equal(t, goodPodWithAnnotationsAndLabelsAndUID, pod) // delete err = c.Kubernetes. @@ -135,14 +135,14 @@ func TestKubeclient(t *testing.T) { with(goodPod, gvk(podGVK)), with(&metav1.PartialObjectMetadata{}, gvk(podGVK)), with(&metav1.PartialObjectMetadata{}, gvk(podGVK)), - with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)), + with(goodPod, annotations(), labels(), uid(), gvk(podGVK)), with(&metav1.PartialObjectMetadata{}, gvk(podGVK)), }, { with(goodPod, annotations(), gvk(podGVK)), with(&metav1.PartialObjectMetadata{}, gvk(podGVK)), with(&metav1.PartialObjectMetadata{}, gvk(podGVK)), - with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)), + with(goodPod, annotations(), labels(), uid(), gvk(podGVK)), with(&metav1.PartialObjectMetadata{}, gvk(podGVK)), }, }, @@ -150,12 +150,12 @@ func TestKubeclient(t *testing.T) { { with(goodPod, annotations(), labels(), gvk(podGVK)), with(goodPod, annotations(), labels(), gvk(podGVK)), - with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)), + with(goodPod, annotations(), labels(), uid(), gvk(podGVK)), }, { with(goodPod, emptyAnnotations(), labels(), gvk(podGVK)), with(goodPod, annotations(), labels(), gvk(podGVK)), - with(goodPod, annotations(), labels(), clusterName(), gvk(podGVK)), + with(goodPod, annotations(), labels(), uid(), gvk(podGVK)), }, }, }, @@ -217,13 +217,13 @@ func TestKubeclient(t *testing.T) { require.Equal(t, with(goodAPIService, annotations(), labels()), apiService) // update - goodAPIServiceWithAnnotationsAndLabelsAndClusterName := with(goodAPIService, annotations(), labels(), clusterName()).(*apiregistrationv1.APIService) + goodAPIServiceWithAnnotationsAndLabelsAndUID := with(goodAPIService, annotations(), labels(), uid()).(*apiregistrationv1.APIService) apiService, err = c.Aggregation. ApiregistrationV1(). APIServices(). - Update(context.Background(), goodAPIServiceWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{}) + Update(context.Background(), goodAPIServiceWithAnnotationsAndLabelsAndUID, metav1.UpdateOptions{}) require.NoError(t, err) - require.Equal(t, goodAPIServiceWithAnnotationsAndLabelsAndClusterName, apiService) + require.Equal(t, goodAPIServiceWithAnnotationsAndLabelsAndUID, apiService) // delete err = c.Aggregation. @@ -236,13 +236,13 @@ func TestKubeclient(t *testing.T) { { with(goodAPIService, gvk(apiServiceGVK)), with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)), - with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)), + with(goodAPIService, annotations(), labels(), uid(), gvk(apiServiceGVK)), with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)), }, { with(goodAPIService, annotations(), gvk(apiServiceGVK)), with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)), - with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)), + with(goodAPIService, annotations(), labels(), uid(), gvk(apiServiceGVK)), with(&metav1.PartialObjectMetadata{}, gvk(apiServiceGVK)), }, }, @@ -250,12 +250,12 @@ func TestKubeclient(t *testing.T) { { with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)), with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)), - with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)), + with(goodAPIService, annotations(), labels(), uid(), gvk(apiServiceGVK)), }, { with(goodAPIService, emptyAnnotations(), labels(), gvk(apiServiceGVK)), with(goodAPIService, annotations(), labels(), gvk(apiServiceGVK)), - with(goodAPIService, annotations(), labels(), clusterName(), gvk(apiServiceGVK)), + with(goodAPIService, annotations(), labels(), uid(), gvk(apiServiceGVK)), }, }, }, @@ -282,13 +282,13 @@ func TestKubeclient(t *testing.T) { require.Equal(t, with(goodCredentialIssuer, annotations(), labels()), tokenCredentialRequest) // update - goodCredentialIssuerWithAnnotationsAndLabelsAndClusterName := with(goodCredentialIssuer, annotations(), labels(), clusterName()).(*conciergeconfigv1alpha1.CredentialIssuer) + goodCredentialIssuerWithAnnotationsAndLabelsAndUID := with(goodCredentialIssuer, annotations(), labels(), uid()).(*conciergeconfigv1alpha1.CredentialIssuer) tokenCredentialRequest, err = c.PinnipedConcierge. ConfigV1alpha1(). CredentialIssuers(). - Update(context.Background(), goodCredentialIssuerWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{}) + Update(context.Background(), goodCredentialIssuerWithAnnotationsAndLabelsAndUID, metav1.UpdateOptions{}) require.NoError(t, err) - require.Equal(t, goodCredentialIssuerWithAnnotationsAndLabelsAndClusterName, tokenCredentialRequest) + require.Equal(t, goodCredentialIssuerWithAnnotationsAndLabelsAndUID, tokenCredentialRequest) // delete err = c.PinnipedConcierge. @@ -301,13 +301,13 @@ func TestKubeclient(t *testing.T) { { with(goodCredentialIssuer, gvk(credentialIssuerGVK)), with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)), - with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)), + with(goodCredentialIssuer, annotations(), labels(), uid(), gvk(credentialIssuerGVK)), with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)), }, { with(goodCredentialIssuer, annotations(), gvk(credentialIssuerGVK)), with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)), - with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)), + with(goodCredentialIssuer, annotations(), labels(), uid(), gvk(credentialIssuerGVK)), with(&metav1.PartialObjectMetadata{}, gvk(credentialIssuerGVK)), }, }, @@ -315,12 +315,12 @@ func TestKubeclient(t *testing.T) { { with(goodCredentialIssuer, annotations(), labels(), gvk(credentialIssuerGVK)), with(goodCredentialIssuer, annotations(), labels(), gvk(credentialIssuerGVK)), - with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)), + with(goodCredentialIssuer, annotations(), labels(), uid(), gvk(credentialIssuerGVK)), }, { with(goodCredentialIssuer, emptyAnnotations(), labels(), gvk(credentialIssuerGVK)), with(goodCredentialIssuer, annotations(), labels(), gvk(credentialIssuerGVK)), - with(goodCredentialIssuer, annotations(), labels(), clusterName(), gvk(credentialIssuerGVK)), + with(goodCredentialIssuer, annotations(), labels(), uid(), gvk(credentialIssuerGVK)), }, }, }, @@ -347,13 +347,13 @@ func TestKubeclient(t *testing.T) { require.Equal(t, with(goodFederationDomain, annotations(), labels()), federationDomain) // update - goodFederationDomainWithAnnotationsAndLabelsAndClusterName := with(goodFederationDomain, annotations(), labels(), clusterName()).(*supervisorconfigv1alpha1.FederationDomain) + goodFederationDomainWithAnnotationsAndLabelsAndUID := with(goodFederationDomain, annotations(), labels(), uid()).(*supervisorconfigv1alpha1.FederationDomain) federationDomain, err = c.PinnipedSupervisor. ConfigV1alpha1(). FederationDomains(federationDomain.Namespace). - Update(context.Background(), goodFederationDomainWithAnnotationsAndLabelsAndClusterName, metav1.UpdateOptions{}) + Update(context.Background(), goodFederationDomainWithAnnotationsAndLabelsAndUID, metav1.UpdateOptions{}) require.NoError(t, err) - require.Equal(t, goodFederationDomainWithAnnotationsAndLabelsAndClusterName, federationDomain) + require.Equal(t, goodFederationDomainWithAnnotationsAndLabelsAndUID, federationDomain) // delete err = c.PinnipedSupervisor. @@ -366,13 +366,13 @@ func TestKubeclient(t *testing.T) { { with(goodFederationDomain, gvk(federationDomainGVK)), with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)), - with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)), + with(goodFederationDomain, annotations(), labels(), uid(), gvk(federationDomainGVK)), with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)), }, { with(goodFederationDomain, annotations(), gvk(federationDomainGVK)), with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)), - with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)), + with(goodFederationDomain, annotations(), labels(), uid(), gvk(federationDomainGVK)), with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)), }, }, @@ -380,12 +380,12 @@ func TestKubeclient(t *testing.T) { { with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)), with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)), - with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)), + with(goodFederationDomain, annotations(), labels(), uid(), gvk(federationDomainGVK)), }, { with(goodFederationDomain, emptyAnnotations(), labels(), gvk(federationDomainGVK)), with(goodFederationDomain, annotations(), labels(), gvk(federationDomainGVK)), - with(goodFederationDomain, annotations(), labels(), clusterName(), gvk(federationDomainGVK)), + with(goodFederationDomain, annotations(), labels(), uid(), gvk(federationDomainGVK)), }, }, }, @@ -428,7 +428,7 @@ func TestKubeclient(t *testing.T) { FederationDomains(goodFederationDomain.Namespace). Create(context.Background(), goodFederationDomain, metav1.CreateOptions{}) require.NoError(t, err) - require.Equal(t, with(goodFederationDomain, clusterName()), federationDomain) + require.Equal(t, with(goodFederationDomain, uid()), federationDomain) // read federationDomain, err = c.PinnipedSupervisor. @@ -436,7 +436,7 @@ func TestKubeclient(t *testing.T) { FederationDomains(federationDomain.Namespace). Get(context.Background(), federationDomain.Name, metav1.GetOptions{}) require.NoError(t, err) - require.Equal(t, with(goodFederationDomain, clusterName()), federationDomain) + require.Equal(t, with(goodFederationDomain, uid()), federationDomain) }, wantMiddlewareReqs: [][]Object{ { @@ -444,7 +444,7 @@ func TestKubeclient(t *testing.T) { with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)), }, { - with(goodFederationDomain, clusterName(), gvk(federationDomainGVK)), + with(goodFederationDomain, uid(), gvk(federationDomainGVK)), with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK)), }, }, @@ -502,7 +502,7 @@ func TestKubeclient(t *testing.T) { name: "non-pertinent mutater", t: t, mutateReq: func(rt RoundTrip, obj Object) error { - clusterName()(obj) + uid()(obj) return nil }, }} @@ -728,7 +728,7 @@ func newSimpleMiddleware(t *testing.T, hasMutateReqFunc, mutatedReq, hasMutateRe m.mutateReq = func(rt RoundTrip, obj Object) error { if mutatedReq { if rt.Verb() == VerbCreate { - obj.SetClusterName(someClusterName) + obj.SetUID(someUID) } } return nil @@ -916,9 +916,9 @@ func labels() withFunc { } } -func clusterName() withFunc { +func uid() withFunc { return func(obj Object) { - obj.SetClusterName(someClusterName) + obj.SetUID(someUID) } } diff --git a/internal/oidc/provider/formposthtml/formposthtml_test.go b/internal/oidc/provider/formposthtml/formposthtml_test.go index e28714c0..e7d82b75 100644 --- a/internal/oidc/provider/formposthtml/formposthtml_test.go +++ b/internal/oidc/provider/formposthtml/formposthtml_test.go @@ -30,7 +30,7 @@ var ( - + @@ -61,7 +61,7 @@ var ( // 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-1LS3gM7wTGc0dYXZiqW6HK1LHk74YSG8GsJBC/j1/i8='; ` + + `script-src 'sha256-uIWC0J7wd7tWtcXmugZCkKsQpqOsQzqBI/mfQMtUde0='; ` + `style-src 'sha256-kXh6OrB2z7wkx7v1N3ay9deQhV5edwuogARaUtvNYN4='; ` + `img-src data:; ` + `connect-src *; ` + From e78c7d4e0e7d542300124bed900be8498624e520 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 6 Jun 2022 16:01:26 -0700 Subject: [PATCH 68/77] update kube codegen versions and add 1.24 codegen Signed-off-by: Monis Khan --- generated/1.21/apis/go.mod | 4 +- generated/1.21/apis/go.sum | 10 +- generated/1.21/client/go.mod | 4 +- generated/1.21/client/go.sum | 21 +- generated/1.22/apis/go.mod | 4 +- generated/1.22/apis/go.sum | 8 +- generated/1.22/client/go.mod | 4 +- generated/1.22/client/go.sum | 12 +- generated/1.23/apis/go.mod | 4 +- generated/1.23/apis/go.sum | 8 +- .../clientset/versioned/clientset.go | 4 + generated/1.23/client/go.mod | 4 +- generated/1.23/client/go.sum | 12 +- .../clientset/versioned/clientset.go | 4 + generated/1.24/README.adoc | 1334 +++++++++++++++++ .../concierge/authentication/v1alpha1/doc.go | 10 + .../authentication/v1alpha1/register.go | 45 + .../v1alpha1/types_jwtauthenticator.go | 85 ++ .../authentication/v1alpha1/types_meta.go | 75 + .../authentication/v1alpha1/types_tls.go | 11 + .../v1alpha1/types_webhookauthenticator.go | 56 + .../v1alpha1/zz_generated.deepcopy.go | 273 ++++ .../apis/concierge/config/v1alpha1/doc.go | 10 + .../concierge/config/v1alpha1/register.go | 43 + .../config/v1alpha1/types_credentialissuer.go | 244 +++ .../config/v1alpha1/zz_generated.deepcopy.go | 259 ++++ generated/1.24/apis/concierge/identity/doc.go | 8 + .../1.24/apis/concierge/identity/register.go | 38 + .../apis/concierge/identity/types_userinfo.go | 37 + .../apis/concierge/identity/types_whoami.go | 40 + .../concierge/identity/v1alpha1/conversion.go | 4 + .../concierge/identity/v1alpha1/defaults.go | 12 + .../apis/concierge/identity/v1alpha1/doc.go | 11 + .../concierge/identity/v1alpha1/register.go | 43 + .../identity/v1alpha1/types_userinfo.go | 41 + .../identity/v1alpha1/types_whoami.go | 43 + .../v1alpha1/zz_generated.conversion.go | 235 +++ .../v1alpha1/zz_generated.deepcopy.go | 185 +++ .../v1alpha1/zz_generated.defaults.go | 20 + .../identity/validation/validation.go | 14 + .../identity/zz_generated.deepcopy.go | 185 +++ generated/1.24/apis/concierge/login/doc.go | 8 + .../1.24/apis/concierge/login/register.go | 38 + .../apis/concierge/login/types_clustercred.go | 21 + .../1.24/apis/concierge/login/types_token.go | 47 + .../concierge/login/v1alpha1/conversion.go | 4 + .../apis/concierge/login/v1alpha1/defaults.go | 12 + .../1.24/apis/concierge/login/v1alpha1/doc.go | 11 + .../apis/concierge/login/v1alpha1/register.go | 43 + .../login/v1alpha1/types_clustercred.go | 22 + .../concierge/login/v1alpha1/types_token.go | 51 + .../login/v1alpha1/zz_generated.conversion.go | 201 +++ .../login/v1alpha1/zz_generated.deepcopy.go | 134 ++ .../login/v1alpha1/zz_generated.defaults.go | 20 + .../concierge/login/zz_generated.deepcopy.go | 134 ++ generated/1.24/apis/go.mod | 9 + generated/1.24/apis/go.sum | 241 +++ .../apis/supervisor/config/v1alpha1/doc.go | 11 + .../supervisor/config/v1alpha1/register.go | 43 + .../config/v1alpha1/types_federationdomain.go | 135 ++ .../config/v1alpha1/zz_generated.deepcopy.go | 152 ++ .../1.24/apis/supervisor/idp/v1alpha1/doc.go | 11 + .../apis/supervisor/idp/v1alpha1/register.go | 47 + .../types_activedirectoryidentityprovider.go | 207 +++ .../v1alpha1/types_ldapidentityprovider.go | 196 +++ .../supervisor/idp/v1alpha1/types_meta.go | 75 + .../v1alpha1/types_oidcidentityprovider.go | 206 +++ .../apis/supervisor/idp/v1alpha1/types_tls.go | 11 + .../idp/v1alpha1/zz_generated.deepcopy.go | 608 ++++++++ .../types_supervisor_idp_discovery.go | 66 + .../supervisor/oidc/types_supervisor_oidc.go | 25 + .../clientset/versioned/clientset.go | 147 ++ .../concierge/clientset/versioned/doc.go | 7 + .../versioned/fake/clientset_generated.go | 93 ++ .../concierge/clientset/versioned/fake/doc.go | 7 + .../clientset/versioned/fake/register.go | 49 + .../clientset/versioned/scheme/doc.go | 7 + .../clientset/versioned/scheme/register.go | 49 + .../v1alpha1/authentication_client.go | 99 ++ .../typed/authentication/v1alpha1/doc.go | 7 + .../typed/authentication/v1alpha1/fake/doc.go | 7 + .../fake/fake_authentication_client.go | 31 + .../v1alpha1/fake/fake_jwtauthenticator.go | 120 ++ .../fake/fake_webhookauthenticator.go | 120 ++ .../v1alpha1/generated_expansion.go | 10 + .../v1alpha1/jwtauthenticator.go | 171 +++ .../v1alpha1/webhookauthenticator.go | 171 +++ .../typed/config/v1alpha1/config_client.go | 94 ++ .../typed/config/v1alpha1/credentialissuer.go | 171 +++ .../versioned/typed/config/v1alpha1/doc.go | 7 + .../typed/config/v1alpha1/fake/doc.go | 7 + .../v1alpha1/fake/fake_config_client.go | 27 + .../v1alpha1/fake/fake_credentialissuer.go | 120 ++ .../config/v1alpha1/generated_expansion.go | 8 + .../versioned/typed/identity/v1alpha1/doc.go | 7 + .../typed/identity/v1alpha1/fake/doc.go | 7 + .../v1alpha1/fake/fake_identity_client.go | 27 + .../v1alpha1/fake/fake_whoamirequest.go | 34 + .../identity/v1alpha1/generated_expansion.go | 8 + .../identity/v1alpha1/identity_client.go | 94 ++ .../typed/identity/v1alpha1/whoamirequest.go | 51 + .../versioned/typed/login/v1alpha1/doc.go | 7 + .../typed/login/v1alpha1/fake/doc.go | 7 + .../login/v1alpha1/fake/fake_login_client.go | 27 + .../fake/fake_tokencredentialrequest.go | 34 + .../login/v1alpha1/generated_expansion.go | 8 + .../typed/login/v1alpha1/login_client.go | 94 ++ .../login/v1alpha1/tokencredentialrequest.go | 51 + .../authentication/interface.go | 33 + .../authentication/v1alpha1/interface.go | 39 + .../v1alpha1/jwtauthenticator.go | 76 + .../v1alpha1/webhookauthenticator.go | 76 + .../externalversions/config/interface.go | 33 + .../config/v1alpha1/credentialissuer.go | 76 + .../config/v1alpha1/interface.go | 32 + .../informers/externalversions/factory.go | 173 +++ .../informers/externalversions/generic.go | 56 + .../internalinterfaces/factory_interfaces.go | 27 + .../v1alpha1/expansion_generated.go | 14 + .../v1alpha1/jwtauthenticator.go | 55 + .../v1alpha1/webhookauthenticator.go | 55 + .../config/v1alpha1/credentialissuer.go | 55 + .../config/v1alpha1/expansion_generated.go | 10 + generated/1.24/client/go.mod | 12 + generated/1.24/client/go.sum | 638 ++++++++ .../clientset/versioned/clientset.go | 121 ++ .../supervisor/clientset/versioned/doc.go | 7 + .../versioned/fake/clientset_generated.go | 79 + .../clientset/versioned/fake/doc.go | 7 + .../clientset/versioned/fake/register.go | 45 + .../clientset/versioned/scheme/doc.go | 7 + .../clientset/versioned/scheme/register.go | 45 + .../typed/config/v1alpha1/config_client.go | 94 ++ .../versioned/typed/config/v1alpha1/doc.go | 7 + .../typed/config/v1alpha1/fake/doc.go | 7 + .../v1alpha1/fake/fake_config_client.go | 27 + .../v1alpha1/fake/fake_federationdomain.go | 129 ++ .../typed/config/v1alpha1/federationdomain.go | 182 +++ .../config/v1alpha1/generated_expansion.go | 8 + .../activedirectoryidentityprovider.go | 182 +++ .../versioned/typed/idp/v1alpha1/doc.go | 7 + .../versioned/typed/idp/v1alpha1/fake/doc.go | 7 + .../fake_activedirectoryidentityprovider.go | 129 ++ .../idp/v1alpha1/fake/fake_idp_client.go | 35 + .../fake/fake_ldapidentityprovider.go | 129 ++ .../fake/fake_oidcidentityprovider.go | 129 ++ .../typed/idp/v1alpha1/generated_expansion.go | 12 + .../typed/idp/v1alpha1/idp_client.go | 104 ++ .../idp/v1alpha1/ldapidentityprovider.go | 182 +++ .../idp/v1alpha1/oidcidentityprovider.go | 182 +++ .../externalversions/config/interface.go | 33 + .../config/v1alpha1/federationdomain.go | 77 + .../config/v1alpha1/interface.go | 32 + .../informers/externalversions/factory.go | 173 +++ .../informers/externalversions/generic.go | 58 + .../externalversions/idp/interface.go | 33 + .../activedirectoryidentityprovider.go | 77 + .../idp/v1alpha1/interface.go | 46 + .../idp/v1alpha1/ldapidentityprovider.go | 77 + .../idp/v1alpha1/oidcidentityprovider.go | 77 + .../internalinterfaces/factory_interfaces.go | 27 + .../config/v1alpha1/expansion_generated.go | 14 + .../config/v1alpha1/federationdomain.go | 86 ++ .../activedirectoryidentityprovider.go | 86 ++ .../idp/v1alpha1/expansion_generated.go | 30 + .../idp/v1alpha1/ldapidentityprovider.go | 86 ++ .../idp/v1alpha1/oidcidentityprovider.go | 86 ++ ...cierge.pinniped.dev_jwtauthenticators.yaml | 176 +++ ...ge.pinniped.dev_webhookauthenticators.yaml | 149 ++ ...cierge.pinniped.dev_credentialissuers.yaml | 246 +++ ...rvisor.pinniped.dev_federationdomains.yaml | 170 +++ ....dev_activedirectoryidentityproviders.yaml | 304 ++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 301 ++++ ...or.pinniped.dev_oidcidentityproviders.yaml | 328 ++++ .../clientset/versioned/clientset.go | 4 + .../clientset/versioned/clientset.go | 4 + go.mod | 2 - go.sum | 56 +- hack/lib/kube-versions.txt | 7 +- 179 files changed, 14298 insertions(+), 108 deletions(-) create mode 100644 generated/1.24/README.adoc create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/doc.go create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/register.go create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/types_meta.go create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go create mode 100644 generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/concierge/config/v1alpha1/doc.go create mode 100644 generated/1.24/apis/concierge/config/v1alpha1/register.go create mode 100644 generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go create mode 100644 generated/1.24/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/concierge/identity/doc.go create mode 100644 generated/1.24/apis/concierge/identity/register.go create mode 100644 generated/1.24/apis/concierge/identity/types_userinfo.go create mode 100644 generated/1.24/apis/concierge/identity/types_whoami.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/conversion.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/defaults.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/doc.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/register.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/types_userinfo.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/types_whoami.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.conversion.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.defaults.go create mode 100644 generated/1.24/apis/concierge/identity/validation/validation.go create mode 100644 generated/1.24/apis/concierge/identity/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/concierge/login/doc.go create mode 100644 generated/1.24/apis/concierge/login/register.go create mode 100644 generated/1.24/apis/concierge/login/types_clustercred.go create mode 100644 generated/1.24/apis/concierge/login/types_token.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/conversion.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/defaults.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/doc.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/register.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/types_clustercred.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/types_token.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/zz_generated.conversion.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/concierge/login/v1alpha1/zz_generated.defaults.go create mode 100644 generated/1.24/apis/concierge/login/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/go.mod create mode 100644 generated/1.24/apis/go.sum create mode 100644 generated/1.24/apis/supervisor/config/v1alpha1/doc.go create mode 100644 generated/1.24/apis/supervisor/config/v1alpha1/register.go create mode 100644 generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go create mode 100644 generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/doc.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/register.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/types_meta.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go create mode 100644 generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go create mode 100644 generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go create mode 100644 generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/clientset.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/fake/clientset_generated.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/fake/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/fake/register.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/scheme/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/scheme/register.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/authentication_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_authentication_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_jwtauthenticator.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_webhookauthenticator.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/generated_expansion.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/jwtauthenticator.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/webhookauthenticator.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/config_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/credentialissuer.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_credentialissuer.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/generated_expansion.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_identity_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_whoamirequest.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/generated_expansion.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/identity_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/whoamirequest.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/doc.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_login_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_tokencredentialrequest.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/generated_expansion.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/login_client.go create mode 100644 generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/tokencredentialrequest.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/authentication/interface.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/interface.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/jwtauthenticator.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/webhookauthenticator.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/config/interface.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/credentialissuer.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/interface.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/factory.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/generic.go create mode 100644 generated/1.24/client/concierge/informers/externalversions/internalinterfaces/factory_interfaces.go create mode 100644 generated/1.24/client/concierge/listers/authentication/v1alpha1/expansion_generated.go create mode 100644 generated/1.24/client/concierge/listers/authentication/v1alpha1/jwtauthenticator.go create mode 100644 generated/1.24/client/concierge/listers/authentication/v1alpha1/webhookauthenticator.go create mode 100644 generated/1.24/client/concierge/listers/config/v1alpha1/credentialissuer.go create mode 100644 generated/1.24/client/concierge/listers/config/v1alpha1/expansion_generated.go create mode 100644 generated/1.24/client/go.mod create mode 100644 generated/1.24/client/go.sum create mode 100644 generated/1.24/client/supervisor/clientset/versioned/clientset.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/fake/clientset_generated.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/fake/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/fake/register.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/scheme/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/scheme/register.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/config_client.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_federationdomain.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/federationdomain.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/generated_expansion.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/doc.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_oidcidentityprovider.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/oidcidentityprovider.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/config/interface.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/federationdomain.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/interface.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/factory.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/generic.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/idp/interface.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/oidcidentityprovider.go create mode 100644 generated/1.24/client/supervisor/informers/externalversions/internalinterfaces/factory_interfaces.go create mode 100644 generated/1.24/client/supervisor/listers/config/v1alpha1/expansion_generated.go create mode 100644 generated/1.24/client/supervisor/listers/config/v1alpha1/federationdomain.go create mode 100644 generated/1.24/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go create mode 100644 generated/1.24/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.24/client/supervisor/listers/idp/v1alpha1/oidcidentityprovider.go create mode 100644 generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml create mode 100644 generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml create mode 100644 generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml create mode 100644 generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml create mode 100644 generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml create mode 100644 generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml create mode 100644 generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml diff --git a/generated/1.21/apis/go.mod b/generated/1.21/apis/go.mod index 916eef22..afca477c 100644 --- a/generated/1.21/apis/go.mod +++ b/generated/1.21/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.21/apis go 1.13 require ( - k8s.io/api v0.21.11 - k8s.io/apimachinery v0.21.11 + k8s.io/api v0.21.13 + k8s.io/apimachinery v0.21.13 ) diff --git a/generated/1.21/apis/go.sum b/generated/1.21/apis/go.sum index 45e90b8e..63e134e1 100644 --- a/generated/1.21/apis/go.sum +++ b/generated/1.21/apis/go.sum @@ -112,7 +112,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w 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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -147,10 +147,10 @@ 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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.21.11 h1:wJYhJfpvLkOJb+KdfLb2ps8gb+gPNkyLnevt4Yyssd4= -k8s.io/api v0.21.11/go.mod h1:ipplJOizdDZsizpXHt1uek+yMsoclq2so9ks2aG7yqA= -k8s.io/apimachinery v0.21.11 h1:oi/sFpeUWJIhxrUe4Kn1cwxAGJ0WJ3AQNz5bmeV6klI= -k8s.io/apimachinery v0.21.11/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= +k8s.io/api v0.21.13 h1:Re4jsBbegkuDCR31ZsdgOrzhWEEOpfjQIRsmGT+sPrs= +k8s.io/api v0.21.13/go.mod h1:Il0hsuHjexr4FplADa0xIXVM2j7+0Sk2ZJ1lq9RLpBw= +k8s.io/apimachinery v0.21.13 h1:7fMsssnwIBILqMm0BHyoHJ+bTPXt6Yeyv110c0zAw+A= +k8s.io/apimachinery v0.21.13/go.mod h1:NI5S3z6+ZZ6Da3whzPF+MnJCjU1NyLuTq9WnKIj5I20= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.21/client/go.mod b/generated/1.21/client/go.mod index 23cde429..ccd5530b 100644 --- a/generated/1.21/client/go.mod +++ b/generated/1.21/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.21/apis v0.0.0 - k8s.io/apimachinery v0.21.11 - k8s.io/client-go v0.21.11 + k8s.io/apimachinery v0.21.13 + k8s.io/client-go v0.21.13 ) replace go.pinniped.dev/generated/1.21/apis => ../apis diff --git a/generated/1.21/client/go.sum b/generated/1.21/client/go.sum index c515ca46..50425272 100644 --- a/generated/1.21/client/go.sum +++ b/generated/1.21/client/go.sum @@ -184,7 +184,7 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -237,6 +237,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL 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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -264,7 +265,6 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -277,9 +277,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -402,12 +401,12 @@ 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= -k8s.io/api v0.21.11 h1:wJYhJfpvLkOJb+KdfLb2ps8gb+gPNkyLnevt4Yyssd4= -k8s.io/api v0.21.11/go.mod h1:ipplJOizdDZsizpXHt1uek+yMsoclq2so9ks2aG7yqA= -k8s.io/apimachinery v0.21.11 h1:oi/sFpeUWJIhxrUe4Kn1cwxAGJ0WJ3AQNz5bmeV6klI= -k8s.io/apimachinery v0.21.11/go.mod h1:USs+ifLG6ZUgHGA/9lGxjdHzCB3hUO3fG1VBOwi0IHo= -k8s.io/client-go v0.21.11 h1:AIA8Yq/pTx+wyB/x3OYrmYJJCdcC7YPOrmwnW0Ws3Yk= -k8s.io/client-go v0.21.11/go.mod h1:VYCT1Xg3OkLEN/O2zY4qLiLekWg1m/TEEw0wsZ0OlX0= +k8s.io/api v0.21.13 h1:Re4jsBbegkuDCR31ZsdgOrzhWEEOpfjQIRsmGT+sPrs= +k8s.io/api v0.21.13/go.mod h1:Il0hsuHjexr4FplADa0xIXVM2j7+0Sk2ZJ1lq9RLpBw= +k8s.io/apimachinery v0.21.13 h1:7fMsssnwIBILqMm0BHyoHJ+bTPXt6Yeyv110c0zAw+A= +k8s.io/apimachinery v0.21.13/go.mod h1:NI5S3z6+ZZ6Da3whzPF+MnJCjU1NyLuTq9WnKIj5I20= +k8s.io/client-go v0.21.13 h1:cUrPH3Nns3d3vhhweOV3/uqNAz9Fc8FKdvq1Zt44gPs= +k8s.io/client-go v0.21.13/go.mod h1:XaXNCeRPYqj+M2PU9fU6c7c+agvhSh+DpRFaBhbezhg= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.22/apis/go.mod b/generated/1.22/apis/go.mod index 3505f894..663f1cf5 100644 --- a/generated/1.22/apis/go.mod +++ b/generated/1.22/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.22/apis go 1.13 require ( - k8s.io/api v0.22.8 - k8s.io/apimachinery v0.22.8 + k8s.io/api v0.22.10 + k8s.io/apimachinery v0.22.10 ) diff --git a/generated/1.22/apis/go.sum b/generated/1.22/apis/go.sum index 11f00878..22e6b26c 100644 --- a/generated/1.22/apis/go.sum +++ b/generated/1.22/apis/go.sum @@ -205,10 +205,10 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.22.8 h1:7Ld6tHuvaYzcQE2axLmomWlhP0fK3vpLfo6fBaNrCIs= -k8s.io/api v0.22.8/go.mod h1:uLlWJNRJ+AYwgAdsNwf0TsD3eByNYW9RlXFmkMdL3yk= -k8s.io/apimachinery v0.22.8 h1:kazMo4/t5ZPI7MwImnCJODZrt1VuwbYBixhTzaNIxsw= -k8s.io/apimachinery v0.22.8/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= +k8s.io/api v0.22.10 h1:gnNb/RabQ+YJQumeyKFCo2yc/E/Oo3qN8rTqovHqYFo= +k8s.io/api v0.22.10/go.mod h1:uT4t8fd7qI503LrFXs0sHOBmOInJ3E3jCsRLoXV6Pys= +k8s.io/apimachinery v0.22.10 h1:j6e3uKe0H7Dxgj7Hzj17IZJoSPLSMhptfmeGwvtPclE= +k8s.io/apimachinery v0.22.10/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.22/client/go.mod b/generated/1.22/client/go.mod index 5dbb6a8a..1060c445 100644 --- a/generated/1.22/client/go.mod +++ b/generated/1.22/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.22/apis v0.0.0 - k8s.io/apimachinery v0.22.8 - k8s.io/client-go v0.22.8 + k8s.io/apimachinery v0.22.10 + k8s.io/client-go v0.22.10 ) replace go.pinniped.dev/generated/1.22/apis => ../apis diff --git a/generated/1.22/client/go.sum b/generated/1.22/client/go.sum index a973b114..d851d930 100644 --- a/generated/1.22/client/go.sum +++ b/generated/1.22/client/go.sum @@ -427,12 +427,12 @@ 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= -k8s.io/api v0.22.8 h1:7Ld6tHuvaYzcQE2axLmomWlhP0fK3vpLfo6fBaNrCIs= -k8s.io/api v0.22.8/go.mod h1:uLlWJNRJ+AYwgAdsNwf0TsD3eByNYW9RlXFmkMdL3yk= -k8s.io/apimachinery v0.22.8 h1:kazMo4/t5ZPI7MwImnCJODZrt1VuwbYBixhTzaNIxsw= -k8s.io/apimachinery v0.22.8/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= -k8s.io/client-go v0.22.8 h1:dWgwPqpWH/DPLWSczA6b61VxFIILe989MXipoE9332s= -k8s.io/client-go v0.22.8/go.mod h1:dOHOy82WOBz0siYHpVyY7FqTIq+iXFXW3+THFk6qErU= +k8s.io/api v0.22.10 h1:gnNb/RabQ+YJQumeyKFCo2yc/E/Oo3qN8rTqovHqYFo= +k8s.io/api v0.22.10/go.mod h1:uT4t8fd7qI503LrFXs0sHOBmOInJ3E3jCsRLoXV6Pys= +k8s.io/apimachinery v0.22.10 h1:j6e3uKe0H7Dxgj7Hzj17IZJoSPLSMhptfmeGwvtPclE= +k8s.io/apimachinery v0.22.10/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= +k8s.io/client-go v0.22.10 h1:HMRbhtR5JCkYsZlyRTFz8qWbgXbDKXFWisp7xPmQ/YQ= +k8s.io/client-go v0.22.10/go.mod h1:fsvH0pIppH4qY/7qB41mi1tgSUTid5YzHtglTQgYx/s= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= diff --git a/generated/1.23/apis/go.mod b/generated/1.23/apis/go.mod index a01ea546..7f439226 100644 --- a/generated/1.23/apis/go.mod +++ b/generated/1.23/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.23/apis go 1.13 require ( - k8s.io/api v0.23.5 - k8s.io/apimachinery v0.23.5 + k8s.io/api v0.23.7 + k8s.io/apimachinery v0.23.7 ) diff --git a/generated/1.23/apis/go.sum b/generated/1.23/apis/go.sum index 73deec9f..06a448d4 100644 --- a/generated/1.23/apis/go.sum +++ b/generated/1.23/apis/go.sum @@ -219,10 +219,10 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= -k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= -k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= -k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/api v0.23.7 h1:UxFan6t0yTTgEKffoEEXUcLdhzAXf/yqTaz/XU7stzg= +k8s.io/api v0.23.7/go.mod h1:Jn7OvVwrE77fPvtdXjEAjfS6KR5l4oTW8CfksHgZBUw= +k8s.io/apimachinery v0.23.7 h1:IV0+rdF4U+8j7FY6jTw394JsISeHYNAQ7pblZyFfyvw= +k8s.io/apimachinery v0.23.7/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 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= diff --git a/generated/1.23/client/concierge/clientset/versioned/clientset.go b/generated/1.23/client/concierge/clientset/versioned/clientset.go index ba3cb60b..e026c5f0 100644 --- a/generated/1.23/client/concierge/clientset/versioned/clientset.go +++ b/generated/1.23/client/concierge/clientset/versioned/clientset.go @@ -72,6 +72,10 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { diff --git a/generated/1.23/client/go.mod b/generated/1.23/client/go.mod index bd82920b..ae64ae31 100644 --- a/generated/1.23/client/go.mod +++ b/generated/1.23/client/go.mod @@ -5,8 +5,8 @@ go 1.13 require ( go.pinniped.dev/generated/1.23/apis v0.0.0 - k8s.io/apimachinery v0.23.5 - k8s.io/client-go v0.23.5 + k8s.io/apimachinery v0.23.7 + k8s.io/client-go v0.23.7 ) replace go.pinniped.dev/generated/1.23/apis => ../apis diff --git a/generated/1.23/client/go.sum b/generated/1.23/client/go.sum index fbe0a3be..463c13e5 100644 --- a/generated/1.23/client/go.sum +++ b/generated/1.23/client/go.sum @@ -593,12 +593,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh 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= -k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= -k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= -k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= -k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= -k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= +k8s.io/api v0.23.7 h1:UxFan6t0yTTgEKffoEEXUcLdhzAXf/yqTaz/XU7stzg= +k8s.io/api v0.23.7/go.mod h1:Jn7OvVwrE77fPvtdXjEAjfS6KR5l4oTW8CfksHgZBUw= +k8s.io/apimachinery v0.23.7 h1:IV0+rdF4U+8j7FY6jTw394JsISeHYNAQ7pblZyFfyvw= +k8s.io/apimachinery v0.23.7/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= +k8s.io/client-go v0.23.7 h1:BZRzGOsLxjloMgSsiYjMIfkPMNXNjvOcjxuJou9rT3Q= +k8s.io/client-go v0.23.7/go.mod h1:GK1rjayM170nhnehxm2wtHNZIAL0ZZyoUHmd5et1Egw= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 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= diff --git a/generated/1.23/client/supervisor/clientset/versioned/clientset.go b/generated/1.23/client/supervisor/clientset/versioned/clientset.go index b36adb5b..6f778d3a 100644 --- a/generated/1.23/client/supervisor/clientset/versioned/clientset.go +++ b/generated/1.23/client/supervisor/clientset/versioned/clientset.go @@ -56,6 +56,10 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc new file mode 100644 index 00000000..c59924cd --- /dev/null +++ b/generated/1.24/README.adoc @@ -0,0 +1,1334 @@ +// Generated documentation. Please do not edit. +:anchor_prefix: k8s-api + +[id="{p}-api-reference"] +== API Reference + +.Packages +- xref:{anchor_prefix}-authentication-concierge-pinniped-dev-v1alpha1[$$authentication.concierge.pinniped.dev/v1alpha1$$] +- xref:{anchor_prefix}-config-concierge-pinniped-dev-v1alpha1[$$config.concierge.pinniped.dev/v1alpha1$$] +- xref:{anchor_prefix}-config-supervisor-pinniped-dev-v1alpha1[$$config.supervisor.pinniped.dev/v1alpha1$$] +- xref:{anchor_prefix}-identity-concierge-pinniped-dev-identity[$$identity.concierge.pinniped.dev/identity$$] +- xref:{anchor_prefix}-identity-concierge-pinniped-dev-v1alpha1[$$identity.concierge.pinniped.dev/v1alpha1$$] +- xref:{anchor_prefix}-idp-supervisor-pinniped-dev-v1alpha1[$$idp.supervisor.pinniped.dev/v1alpha1$$] +- xref:{anchor_prefix}-login-concierge-pinniped-dev-v1alpha1[$$login.concierge.pinniped.dev/v1alpha1$$] + + +[id="{anchor_prefix}-authentication-concierge-pinniped-dev-v1alpha1"] +=== authentication.concierge.pinniped.dev/v1alpha1 + +Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authentication API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-conditionstatus[$$ConditionStatus$$]__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-conditionstatus"] +==== ConditionStatus (string) + +ConditionStatus is effectively an enum type for Condition.Status. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-condition[$$Condition$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticator"] +==== JWTAuthenticator + +JWTAuthenticator describes the configuration of a JWT authenticator. + Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid signature, existence of claims, etc.) and extract the username and groups from the token. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorlist[$$JWTAuthenticatorList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]__ | Spec for configuring the authenticator. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]__ | Status of the authenticator. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"] +==== JWTAuthenticatorSpec + +Spec for configuring a JWT authenticator. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticator[$$JWTAuthenticator$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim. +| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim. +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwttokenclaims[$$JWTTokenClaims$$]__ | Claims allows customization of the claims that will be mapped to user identity for Kubernetes access. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus"] +==== JWTAuthenticatorStatus + +Status of a JWT authenticator. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticator[$$JWTAuthenticator$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of the authenticator's current state. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwttokenclaims"] +==== JWTTokenClaims + +JWTTokenClaims allows customization of the claims that will be mapped to user identity for Kubernetes access. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groups`* __string__ | Groups is the name of the claim which should be read to extract the user's group membership from the JWT token. When not specified, it will default to "groups". +| *`username`* __string__ | Username is the name of the claim which should be read to extract the username from the JWT token. When not specified, it will default to "username". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-tlsspec"] +==== TLSSpec + +Configuration for configuring TLS on various authenticators. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec[$$WebhookAuthenticatorSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticator"] +==== WebhookAuthenticator + +WebhookAuthenticator describes the configuration of a webhook authenticator. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorlist[$$WebhookAuthenticatorList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec[$$WebhookAuthenticatorSpec$$]__ | Spec for configuring the authenticator. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]__ | Status of the authenticator. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"] +==== WebhookAuthenticatorSpec + +Spec for configuring a webhook authenticator. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticator[$$WebhookAuthenticator$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`endpoint`* __string__ | Webhook server endpoint URL. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus"] +==== WebhookAuthenticatorStatus + +Status of a webhook authenticator. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticator[$$WebhookAuthenticator$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of the authenticator's current state. +|=== + + + +[id="{anchor_prefix}-config-concierge-pinniped-dev-v1alpha1"] +=== config.concierge.pinniped.dev/v1alpha1 + +Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuer"] +==== CredentialIssuer + +CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerlist[$$CredentialIssuerList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$]__ | Spec describes the intended configuration of the Concierge. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | CredentialIssuerStatus describes the status of the Concierge. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerfrontend"] +==== CredentialIssuerFrontend + +CredentialIssuerFrontend describes how to connect using a particular integration strategy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstrategy[$$CredentialIssuerStrategy$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy. +| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI". +| *`impersonationProxyInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyinfo[$$ImpersonationProxyInfo$$]__ | ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. This field is only set when Type is "ImpersonationProxy". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerkubeconfiginfo"] +==== CredentialIssuerKubeConfigInfo + +CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. This type is deprecated and will be removed in a future version. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`server`* __string__ | The K8s API server URL. +| *`certificateAuthorityData`* __string__ | The K8s API server CA bundle. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerspec"] +==== CredentialIssuerSpec + +CredentialIssuerSpec describes the intended configuration of the Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuer[$$CredentialIssuer$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`impersonationProxy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$]__ | ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstatus"] +==== CredentialIssuerStatus + +CredentialIssuerStatus describes the status of the Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuer[$$CredentialIssuer$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`strategies`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstrategy[$$CredentialIssuerStrategy$$] array__ | List of integration strategies that were attempted by Pinniped. +| *`kubeConfigInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerkubeconfiginfo[$$CredentialIssuerKubeConfigInfo$$]__ | Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. This field is deprecated and will be removed in a future version. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstrategy"] +==== CredentialIssuerStrategy + +CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __StrategyType__ | Type of integration attempted. +| *`status`* __StrategyStatus__ | Status of the attempted integration strategy. +| *`reason`* __StrategyReason__ | Reason for the current status. +| *`message`* __string__ | Human-readable description of the current status. +| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | When the status was last checked. +| *`frontend`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]__ | Frontend describes how clients can connect using this strategy. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyinfo"] +==== ImpersonationProxyInfo + +ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`endpoint`* __string__ | Endpoint is the HTTPS endpoint of the impersonation proxy. +| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxymode"] +==== ImpersonationProxyMode (string) + +ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyservicespec"] +==== ImpersonationProxyServiceSpec + +ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyservicetype[$$ImpersonationProxyServiceType$$]__ | Type specifies the type of Service to provision for the impersonation proxy. + If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. +| *`loadBalancerIP`* __string__ | LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. This is not supported on all cloud providers. +| *`annotations`* __object (keys:string, values:string)__ | Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyservicetype"] +==== ImpersonationProxyServiceType (string) + +ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxyspec"] +==== ImpersonationProxySpec + +ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`mode`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-impersonationproxymode[$$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-24-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.type is "None". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"] +==== TokenCredentialRequestAPIInfo + +TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`server`* __string__ | Server is the Kubernetes API server URL. +| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle. +|=== + + + +[id="{anchor_prefix}-config-supervisor-pinniped-dev-v1alpha1"] +=== config.supervisor.pinniped.dev/v1alpha1 + +Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuration API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomain"] +==== FederationDomain + +FederationDomain describes the configuration of an OIDC provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainlist[$$FederationDomainList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$]__ | Spec of the OIDC provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$]__ | Status of the OIDC provider. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets"] +==== FederationDomainSecrets + +FederationDomainSecrets holds information about this OIDC Provider's secrets. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`jwks`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist. +| *`tokenSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing tokens is stored. +| *`stateSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing state parameters is stored. +| *`stateEncryptionKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for encrypting state parameters is stored. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainspec"] +==== FederationDomainSpec + +FederationDomainSpec is a struct that describes an OIDC Provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomain[$$FederationDomain$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint). + See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS). +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainstatus"] +==== FederationDomainStatus + +FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomain[$$FederationDomain$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`status`* __FederationDomainStatusCondition__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure. +| *`message`* __string__ | Message provides human-readable details about the Status. +| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). +| *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomaintlsspec"] +==== FederationDomainTLSSpec + +FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains the TLS serving certificate for the HTTPS endpoints served by this FederationDomain. When provided, the TLS Secret named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use for TLS. + Server Name Indication (SNI) is an extension to the Transport Layer Security (TLS) supported by all major browsers. + SecretName is required if you would like to use different TLS certificates for issuers of different hostnames. SNI requests do not include port numbers, so all issuers with the same DNS hostname must use the same SecretName value even if they have different port numbers. + SecretName is not required when you would like to use only the HTTP endpoints (e.g. when the HTTP listener is configured to listen on loopback interfaces or UNIX domain sockets for traffic from a service mesh sidecar). It is also not required when you would like all requests to this OIDC Provider's HTTPS endpoints to use the default TLS certificate, which is configured elsewhere. + When your Issuer URL's host is an IP address, then this field is ignored. SNI does not work for IP addresses. +|=== + + + +[id="{anchor_prefix}-identity-concierge-pinniped-dev-identity"] +=== identity.concierge.pinniped.dev/identity + +Package identity is the internal version of the Pinniped identity API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-extravalue"] +==== ExtraValue + +ExtraValue masks the value so protobuf can generate + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-userinfo[$$UserInfo$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-kubernetesuserinfo"] +==== KubernetesUserInfo + +KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it. Copied from the Kubernetes token review API. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequeststatus[$$WhoAmIRequestStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`User`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-userinfo[$$UserInfo$$]__ | User is the UserInfo associated with the current user. +| *`Audiences`* __string array__ | Audiences are audience identifiers chosen by the authenticator. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-userinfo"] +==== UserInfo + +UserInfo holds the information about the user needed to implement the user.Info interface. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-kubernetesuserinfo[$$KubernetesUserInfo$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`Username`* __string__ | The name that uniquely identifies this user among all active users. +| *`UID`* __string__ | A unique value that identifies this user across time. If this user is deleted and another user by the same name is added, they will have different UIDs. +| *`Groups`* __string array__ | The names of groups this user is a part of. +| *`Extra`* __object (keys:string, values:string array)__ | Any additional information provided by the authenticator. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequest"] +==== WhoAmIRequest + +WhoAmIRequest submits a request to echo back the current authenticated user. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequestlist[$$WhoAmIRequestList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`ObjectMeta`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | +| *`Spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequestspec[$$WhoAmIRequestSpec$$]__ | +| *`Status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequeststatus[$$WhoAmIRequestStatus$$]__ | +|=== + + + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequeststatus"] +==== WhoAmIRequestStatus + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-whoamirequest[$$WhoAmIRequest$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`KubernetesUserInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-kubernetesuserinfo[$$KubernetesUserInfo$$]__ | The current authenticated user, exactly as Kubernetes understands it. +|=== + + + +[id="{anchor_prefix}-identity-concierge-pinniped-dev-v1alpha1"] +=== identity.concierge.pinniped.dev/v1alpha1 + +Package v1alpha1 is the v1alpha1 version of the Pinniped identity API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-extravalue"] +==== ExtraValue + +ExtraValue masks the value so protobuf can generate + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-userinfo[$$UserInfo$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-kubernetesuserinfo"] +==== KubernetesUserInfo + +KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it. Copied from the Kubernetes token review API. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequeststatus[$$WhoAmIRequestStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`user`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-userinfo[$$UserInfo$$]__ | User is the UserInfo associated with the current user. +| *`audiences`* __string array__ | Audiences are audience identifiers chosen by the authenticator. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-userinfo"] +==== UserInfo + +UserInfo holds the information about the user needed to implement the user.Info interface. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-kubernetesuserinfo[$$KubernetesUserInfo$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | The name that uniquely identifies this user among all active users. +| *`uid`* __string__ | A unique value that identifies this user across time. If this user is deleted and another user by the same name is added, they will have different UIDs. +| *`groups`* __string array__ | The names of groups this user is a part of. +| *`extra`* __object (keys:string, values:string array)__ | Any additional information provided by the authenticator. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequest"] +==== WhoAmIRequest + +WhoAmIRequest submits a request to echo back the current authenticated user. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequestlist[$$WhoAmIRequestList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequestspec[$$WhoAmIRequestSpec$$]__ | +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequeststatus[$$WhoAmIRequestStatus$$]__ | +|=== + + + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequeststatus"] +==== WhoAmIRequestStatus + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-whoamirequest[$$WhoAmIRequest$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`kubernetesUserInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-identity-v1alpha1-kubernetesuserinfo[$$KubernetesUserInfo$$]__ | The current authenticated user, exactly as Kubernetes understands it. +|=== + + + +[id="{anchor_prefix}-idp-supervisor-pinniped-dev-v1alpha1"] +=== idp.supervisor.pinniped.dev/v1alpha1 + +Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity provider (IDP) API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider"] +==== ActiveDirectoryIdentityProvider + +ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderlist[$$ActiveDirectoryIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderbind"] +==== ActiveDirectoryIdentityProviderBind + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an Active Directory bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full dn (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch"] +==== ActiveDirectoryIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for groups. It may make sense to specify a subtree as a search base if you wish to exclude some groups for security reasons or to make searches faster. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. +| *`skipGroupRefresh`* __boolean__ | The user's group membership is refreshed as they interact with the supervisor to obtain new credentials (as their old credentials expire). This allows group membership changes to be quickly reflected into Kubernetes clusters. Since group membership is often used to bind authorization policies, it is important to keep the groups observed in Kubernetes clusters in-sync with the identity provider. + In some environments, frequent group membership queries may result in a significant performance impact on the identity provider and/or the supervisor. The best approach to handle performance impacts is to tweak the group query to be more performant, for example by disabling nested group search or by using a more targeted group search base. + If the group search query cannot be made performant and you are willing to have group memberships remain static for approximately a day, then set skipGroupRefresh to true. This is an insecure configuration as authorization policies that are bound to group membership will not notice if a user has been removed from a particular group until their next login. + This is an experimental feature that may be removed or significantly altered in the future. Consumers of this configuration should carefully read all release notes before upgrading to ensure that the meaning of this field has not changed. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes"] +==== ActiveDirectoryIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", where domain is constructed from the domain components of the group DN. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec"] +==== ActiveDirectoryIdentityProviderSpec + +Spec for configuring an ActiveDirectory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this Active Directory identity provider, i.e., where to connect. For example: ldap.example.com:636. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderbind[$$ActiveDirectoryIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in Active Directory. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus"] +==== ActiveDirectoryIdentityProviderStatus + +Status of an Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __ActiveDirectoryIdentityProviderPhase__ | Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch"] +==== ActiveDirectoryIdentityProviderUserSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for users. It may make sense to specify a subtree as a search base if you wish to exclude some users or to make searches faster. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(\|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, and is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes"] +==== ActiveDirectoryIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. Optional, when empty this defaults to "userPrincipalName". +| *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-conditionstatus[$$ConditionStatus$$]__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-conditionstatus"] +==== ConditionStatus (string) + +ConditionStatus is effectively an enum type for Condition.Status. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] +==== LDAPIdentityProvider + +LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access Protocol (LDAP) identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderlist[$$LDAPIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind"] +==== LDAPIdentityProviderBind + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full dn (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch"] +==== LDAPIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the LDAP provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes[$$LDAPIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each LDAP entry which was found as the result of the group search. +| *`skipGroupRefresh`* __boolean__ | The user's group membership is refreshed as they interact with the supervisor to obtain new credentials (as their old credentials expire). This allows group membership changes to be quickly reflected into Kubernetes clusters. Since group membership is often used to bind authorization policies, it is important to keep the groups observed in Kubernetes clusters in-sync with the identity provider. + In some environments, frequent group membership queries may result in a significant performance impact on the identity provider and/or the supervisor. The best approach to handle performance impacts is to tweak the group query to be more performant, for example by disabling nested group search or by using a more targeted group search base. + If the group search query cannot be made performant and you are willing to have group memberships remain static for approximately a day, then set skipGroupRefresh to true. This is an insecure configuration as authorization policies that are bound to group membership will not notice if a user has been removed from a particular group until their next login. + This is an experimental feature that may be removed or significantly altered in the future. Consumers of this configuration should carefully read all release notes before upgrading to ensure that the meaning of this field has not changed. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes"] +==== LDAPIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] +==== LDAPIdentityProviderSpec + +Spec for configuring an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus"] +==== LDAPIdentityProviderStatus + +Status of an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch"] +==== LDAPIdentityProviderUserSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes"] +==== LDAPIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of the attribute in the LDAP entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] +==== OIDCAuthorizationConfig + +OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization request parameters. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`additionalScopes`* __string array__ | additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request the following scopes: "openid", "offline_access", "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, or as common patterns used by providers who implement the standard in the ecosystem evolve. By setting this list to anything other than an empty list, you are overriding the default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. If you do not want any of these scopes to be requested, you may set this list to contain only "openid". Some OIDC providers may also require a scope to get access to the user's group membership, in which case you may wish to include it in this list. Sometimes the scope to request the user's group membership is called "groups", but unfortunately this is not specified in the OIDC standard. Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See your OIDC provider's documentation for more information about what scopes are available to request claims. Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider may ignore scopes that it does not understand or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). +| *`additionalAuthorizeParameters`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-parameter[$$Parameter$$] array__ | additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's authorization endpoint for its requirements for what to include in the request in order to receive a refresh token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, then include it here. Also note that most providers also require a certain scope to be requested in order to receive refresh tokens. See the additionalScopes setting for more information about using scopes to request refresh tokens. +| *`allowPasswordGrant`* __boolean__ | allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be convenient for users, especially for identities from your OIDC provider which are not intended to represent a human actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. allowPasswordGrant defaults to false. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcclaims"] +==== OIDCClaims + +OIDCClaims provides a mapping from upstream claims into identities. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groups`* __string__ | Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain the groups to which an identity belongs. By default, the identities will not include any group memberships when this setting is not configured. +| *`username`* __string__ | Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain an identity's username. When not set, the username will be an automatically constructed unique string which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from the ID token. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcclient"] +==== OIDCClient + +OIDCClient contains information about an OIDC client (e.g., client ID and client secret). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityprovider"] +==== OIDCIdentityProvider + +OIDCIdentityProvider describes the configuration of an upstream OpenID Connect identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderlist[$$OIDCIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"] +==== OIDCIdentityProviderSpec + +OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityprovider[$$OIDCIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer. +| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider. +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider. +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"] +==== OIDCIdentityProviderStatus + +OIDCIdentityProviderStatus is the status of an OIDC identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityprovider[$$OIDCIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCIdentityProviderPhase__ | Phase summarizes the overall status of the OIDCIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-parameter"] +==== Parameter + +Parameter is a key/value pair which represents a parameter in an HTTP request. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`name`* __string__ | The name of the parameter. Required. +| *`value`* __string__ | The value of the parameter. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec"] +==== TLSSpec + +Configuration for TLS parameters related to identity provider integration. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. +|=== + + + +[id="{anchor_prefix}-login-concierge-pinniped-dev-v1alpha1"] +=== login.concierge.pinniped.dev/v1alpha1 + +Package v1alpha1 is the v1alpha1 version of the Pinniped login API. + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-clustercredential"] +==== ClusterCredential + +ClusterCredential is the cluster-specific credential returned on a successful credential request. It contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequeststatus[$$TokenCredentialRequestStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`expirationTimestamp`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | ExpirationTimestamp indicates a time when the provided credentials expire. +| *`token`* __string__ | Token is a bearer token used by the client for request authentication. +| *`clientCertificateData`* __string__ | PEM-encoded client TLS certificates (including intermediates, if any). +| *`clientKeyData`* __string__ | PEM-encoded private key for the above certificate. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequest"] +==== TokenCredentialRequest + +TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequestlist[$$TokenCredentialRequestList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequestspec[$$TokenCredentialRequestSpec$$]__ | +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequeststatus[$$TokenCredentialRequestStatus$$]__ | +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequestspec"] +==== TokenCredentialRequestSpec + +TokenCredentialRequestSpec is the specification of a TokenCredentialRequest, expected on requests to the Pinniped API. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequest[$$TokenCredentialRequest$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`token`* __string__ | Bearer token supplied with the credential request. +| *`authenticator`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#typedlocalobjectreference-v1-core[$$TypedLocalObjectReference$$]__ | Reference to an authenticator which can validate this credential request. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequeststatus"] +==== TokenCredentialRequestStatus + +TokenCredentialRequestStatus is the status of a TokenCredentialRequest, returned on responses to the Pinniped API. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-tokencredentialrequest[$$TokenCredentialRequest$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`credential`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-login-v1alpha1-clustercredential[$$ClusterCredential$$]__ | A Credential will be returned for a successful credential request. +| *`message`* __string__ | An error message will be returned for an unsuccessful credential request. +|=== + + diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/doc.go b/generated/1.24/apis/concierge/authentication/v1alpha1/doc.go new file mode 100644 index 00000000..d00d3ba6 --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/doc.go @@ -0,0 +1,10 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:defaulter-gen=TypeMeta +// +groupName=authentication.concierge.pinniped.dev + +// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authentication API. +package v1alpha1 diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/register.go b/generated/1.24/apis/concierge/authentication/v1alpha1/register.go new file mode 100644 index 00000000..f58039e4 --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/register.go @@ -0,0 +1,45 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "authentication.concierge.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &WebhookAuthenticator{}, + &WebhookAuthenticatorList{}, + &JWTAuthenticator{}, + &JWTAuthenticatorList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go new file mode 100644 index 00000000..a8596b22 --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -0,0 +1,85 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// Status of a JWT authenticator. +type JWTAuthenticatorStatus struct { + // Represents the observations of the authenticator's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// Spec for configuring a JWT authenticator. +type JWTAuthenticatorSpec struct { + // Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is + // also used to validate the "iss" JWT claim. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://` + Issuer string `json:"issuer"` + + // Audience is the required value of the "aud" JWT claim. + // +kubebuilder:validation:MinLength=1 + Audience string `json:"audience"` + + // Claims allows customization of the claims that will be mapped to user identity + // for Kubernetes access. + // +optional + Claims JWTTokenClaims `json:"claims"` + + // TLS configuration for communicating with the OIDC provider. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// JWTTokenClaims allows customization of the claims that will be mapped to user identity +// for Kubernetes access. +type JWTTokenClaims struct { + // Groups is the name of the claim which should be read to extract the user's + // group membership from the JWT token. When not specified, it will default to "groups". + // +optional + Groups string `json:"groups"` + + // Username is the name of the claim which should be read to extract the + // username from the JWT token. When not specified, it will default to "username". + // +optional + Username string `json:"username"` +} + +// JWTAuthenticator describes the configuration of a JWT authenticator. +// +// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid +// signature, existence of claims, etc.) and extract the username and groups from the token. +// +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster +// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` +// +kubebuilder:printcolumn:name="Audience",type=string,JSONPath=`.spec.audience` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type JWTAuthenticator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the authenticator. + Spec JWTAuthenticatorSpec `json:"spec"` + + // Status of the authenticator. + Status JWTAuthenticatorStatus `json:"status,omitempty"` +} + +// List of JWTAuthenticator objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type JWTAuthenticatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []JWTAuthenticator `json:"items"` +} diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_meta.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_meta.go new file mode 100644 index 00000000..76a7d547 --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go new file mode 100644 index 00000000..12231665 --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_tls.go @@ -0,0 +1,11 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// Configuration for configuring TLS on various authenticators. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go new file mode 100644 index 00000000..77b1e045 --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go @@ -0,0 +1,56 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// Status of a webhook authenticator. +type WebhookAuthenticatorStatus struct { + // Represents the observations of the authenticator's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// Spec for configuring a webhook authenticator. +type WebhookAuthenticatorSpec struct { + // Webhook server endpoint URL. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://` + Endpoint string `json:"endpoint"` + + // TLS configuration. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// WebhookAuthenticator describes the configuration of a webhook authenticator. +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster +// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type WebhookAuthenticator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the authenticator. + Spec WebhookAuthenticatorSpec `json:"spec"` + + // Status of the authenticator. + Status WebhookAuthenticatorStatus `json:"status,omitempty"` +} + +// List of WebhookAuthenticator objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type WebhookAuthenticatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []WebhookAuthenticator `json:"items"` +} diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..883959bf --- /dev/null +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,273 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticator. +func (in *JWTAuthenticator) DeepCopy() *JWTAuthenticator { + if in == nil { + return nil + } + out := new(JWTAuthenticator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JWTAuthenticator) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticatorList) DeepCopyInto(out *JWTAuthenticatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]JWTAuthenticator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticatorList. +func (in *JWTAuthenticatorList) DeepCopy() *JWTAuthenticatorList { + if in == nil { + return nil + } + out := new(JWTAuthenticatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JWTAuthenticatorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticatorSpec) DeepCopyInto(out *JWTAuthenticatorSpec) { + *out = *in + out.Claims = in.Claims + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticatorSpec. +func (in *JWTAuthenticatorSpec) DeepCopy() *JWTAuthenticatorSpec { + if in == nil { + return nil + } + out := new(JWTAuthenticatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticatorStatus) DeepCopyInto(out *JWTAuthenticatorStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticatorStatus. +func (in *JWTAuthenticatorStatus) DeepCopy() *JWTAuthenticatorStatus { + if in == nil { + return nil + } + out := new(JWTAuthenticatorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTTokenClaims) DeepCopyInto(out *JWTTokenClaims) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTTokenClaims. +func (in *JWTTokenClaims) DeepCopy() *JWTTokenClaims { + if in == nil { + return nil + } + out := new(JWTTokenClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookAuthenticator) DeepCopyInto(out *WebhookAuthenticator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthenticator. +func (in *WebhookAuthenticator) DeepCopy() *WebhookAuthenticator { + if in == nil { + return nil + } + out := new(WebhookAuthenticator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WebhookAuthenticator) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookAuthenticatorList) DeepCopyInto(out *WebhookAuthenticatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WebhookAuthenticator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthenticatorList. +func (in *WebhookAuthenticatorList) DeepCopy() *WebhookAuthenticatorList { + if in == nil { + return nil + } + out := new(WebhookAuthenticatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WebhookAuthenticatorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookAuthenticatorSpec) DeepCopyInto(out *WebhookAuthenticatorSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthenticatorSpec. +func (in *WebhookAuthenticatorSpec) DeepCopy() *WebhookAuthenticatorSpec { + if in == nil { + return nil + } + out := new(WebhookAuthenticatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookAuthenticatorStatus) DeepCopyInto(out *WebhookAuthenticatorStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthenticatorStatus. +func (in *WebhookAuthenticatorStatus) DeepCopy() *WebhookAuthenticatorStatus { + if in == nil { + return nil + } + out := new(WebhookAuthenticatorStatus) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/concierge/config/v1alpha1/doc.go b/generated/1.24/apis/concierge/config/v1alpha1/doc.go new file mode 100644 index 00000000..97b2f212 --- /dev/null +++ b/generated/1.24/apis/concierge/config/v1alpha1/doc.go @@ -0,0 +1,10 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:defaulter-gen=TypeMeta +// +groupName=config.concierge.pinniped.dev + +// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration API. +package v1alpha1 diff --git a/generated/1.24/apis/concierge/config/v1alpha1/register.go b/generated/1.24/apis/concierge/config/v1alpha1/register.go new file mode 100644 index 00000000..8d799898 --- /dev/null +++ b/generated/1.24/apis/concierge/config/v1alpha1/register.go @@ -0,0 +1,43 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "config.concierge.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &CredentialIssuer{}, + &CredentialIssuerList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go new file mode 100644 index 00000000..d1cb160b --- /dev/null +++ b/generated/1.24/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -0,0 +1,244 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. +// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy +type StrategyType string + +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. +// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy +type FrontendType string + +// StrategyStatus enumerates whether a strategy is working on a cluster. +// +kubebuilder:validation:Enum=Success;Error +type StrategyStatus string + +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. +// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey +type StrategyReason string + +const ( + KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate") + ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy") + + TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI") + ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy") + + SuccessStrategyStatus = StrategyStatus("Success") + ErrorStrategyStatus = StrategyStatus("Error") + + ListeningStrategyReason = StrategyReason("Listening") + PendingStrategyReason = StrategyReason("Pending") + DisabledStrategyReason = StrategyReason("Disabled") + ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup") + CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey") + CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo") + FetchedKeyStrategyReason = StrategyReason("FetchedKey") +) + +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // 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. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // 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.type is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. +type CredentialIssuerStatus struct { + // List of integration strategies that were attempted by Pinniped. + Strategies []CredentialIssuerStrategy `json:"strategies"` + + // Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. + // This field is deprecated and will be removed in a future version. + // +optional + KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` +} + +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. +type CredentialIssuerKubeConfigInfo struct { + // The K8s API server URL. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://|^http://` + Server string `json:"server"` + + // The K8s API server CA bundle. + // +kubebuilder:validation:MinLength=1 + CertificateAuthorityData string `json:"certificateAuthorityData"` +} + +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. +type CredentialIssuerStrategy struct { + // Type of integration attempted. + Type StrategyType `json:"type"` + + // Status of the attempted integration strategy. + Status StrategyStatus `json:"status"` + + // Reason for the current status. + Reason StrategyReason `json:"reason"` + + // Human-readable description of the current status. + // +kubebuilder:validation:MinLength=1 + Message string `json:"message"` + + // When the status was last checked. + LastUpdateTime metav1.Time `json:"lastUpdateTime"` + + // Frontend describes how clients can connect using this strategy. + Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` +} + +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. +type CredentialIssuerFrontend struct { + // Type describes which frontend mechanism clients can use with a strategy. + Type FrontendType `json:"type"` + + // TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. + // This field is only set when Type is "TokenCredentialRequestAPI". + TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"` + + // ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. + // This field is only set when Type is "ImpersonationProxy". + ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"` +} + +// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. +type TokenCredentialRequestAPIInfo struct { + // Server is the Kubernetes API server URL. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://|^http://` + Server string `json:"server"` + + // CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle. + // +kubebuilder:validation:MinLength=1 + CertificateAuthorityData string `json:"certificateAuthorityData"` +} + +// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. +type ImpersonationProxyInfo struct { + // Endpoint is the HTTPS endpoint of the impersonation proxy. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://` + Endpoint string `json:"endpoint"` + + // CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy. + // +kubebuilder:validation:MinLength=1 + CertificateAuthorityData string `json:"certificateAuthorityData"` +} + +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped,scope=Cluster +// +kubebuilder:printcolumn:name="ProxyMode",type=string,JSONPath=`.spec.impersonationProxy.mode` +// +kubebuilder:printcolumn:name="DefaultStrategy",type=string,JSONPath=`.status.strategies[?(@.status == "Success")].type` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type CredentialIssuer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // + // +optional + Status CredentialIssuerStatus `json:"status"` +} + +// CredentialIssuerList is a list of CredentialIssuer objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type CredentialIssuerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []CredentialIssuer `json:"items"` +} diff --git a/generated/1.24/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..a00b3fc8 --- /dev/null +++ b/generated/1.24/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,259 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuer) DeepCopyInto(out *CredentialIssuer) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuer. +func (in *CredentialIssuer) DeepCopy() *CredentialIssuer { + if in == nil { + return nil + } + out := new(CredentialIssuer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CredentialIssuer) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerFrontend) DeepCopyInto(out *CredentialIssuerFrontend) { + *out = *in + if in.TokenCredentialRequestAPIInfo != nil { + in, out := &in.TokenCredentialRequestAPIInfo, &out.TokenCredentialRequestAPIInfo + *out = new(TokenCredentialRequestAPIInfo) + **out = **in + } + if in.ImpersonationProxyInfo != nil { + in, out := &in.ImpersonationProxyInfo, &out.ImpersonationProxyInfo + *out = new(ImpersonationProxyInfo) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerFrontend. +func (in *CredentialIssuerFrontend) DeepCopy() *CredentialIssuerFrontend { + if in == nil { + return nil + } + out := new(CredentialIssuerFrontend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerKubeConfigInfo) DeepCopyInto(out *CredentialIssuerKubeConfigInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerKubeConfigInfo. +func (in *CredentialIssuerKubeConfigInfo) DeepCopy() *CredentialIssuerKubeConfigInfo { + if in == nil { + return nil + } + out := new(CredentialIssuerKubeConfigInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerList) DeepCopyInto(out *CredentialIssuerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CredentialIssuer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerList. +func (in *CredentialIssuerList) DeepCopy() *CredentialIssuerList { + if in == nil { + return nil + } + out := new(CredentialIssuerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CredentialIssuerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerSpec) DeepCopyInto(out *CredentialIssuerSpec) { + *out = *in + if in.ImpersonationProxy != nil { + in, out := &in.ImpersonationProxy, &out.ImpersonationProxy + *out = new(ImpersonationProxySpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerSpec. +func (in *CredentialIssuerSpec) DeepCopy() *CredentialIssuerSpec { + if in == nil { + return nil + } + out := new(CredentialIssuerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerStatus) DeepCopyInto(out *CredentialIssuerStatus) { + *out = *in + if in.Strategies != nil { + in, out := &in.Strategies, &out.Strategies + *out = make([]CredentialIssuerStrategy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.KubeConfigInfo != nil { + in, out := &in.KubeConfigInfo, &out.KubeConfigInfo + *out = new(CredentialIssuerKubeConfigInfo) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerStatus. +func (in *CredentialIssuerStatus) DeepCopy() *CredentialIssuerStatus { + if in == nil { + return nil + } + out := new(CredentialIssuerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerStrategy) DeepCopyInto(out *CredentialIssuerStrategy) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + if in.Frontend != nil { + in, out := &in.Frontend, &out.Frontend + *out = new(CredentialIssuerFrontend) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerStrategy. +func (in *CredentialIssuerStrategy) DeepCopy() *CredentialIssuerStrategy { + if in == nil { + return nil + } + out := new(CredentialIssuerStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyInfo) DeepCopyInto(out *ImpersonationProxyInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyInfo. +func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo { + if in == nil { + return nil + } + out := new(ImpersonationProxyInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyServiceSpec) DeepCopyInto(out *ImpersonationProxyServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyServiceSpec. +func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSpec { + if in == nil { + return nil + } + out := new(ImpersonationProxyServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxySpec. +func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec { + if in == nil { + return nil + } + out := new(ImpersonationProxySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestAPIInfo. +func (in *TokenCredentialRequestAPIInfo) DeepCopy() *TokenCredentialRequestAPIInfo { + if in == nil { + return nil + } + out := new(TokenCredentialRequestAPIInfo) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/concierge/identity/doc.go b/generated/1.24/apis/concierge/identity/doc.go new file mode 100644 index 00000000..130e8942 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/doc.go @@ -0,0 +1,8 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:deepcopy-gen=package +// +groupName=identity.concierge.pinniped.dev + +// Package identity is the internal version of the Pinniped identity API. +package identity diff --git a/generated/1.24/apis/concierge/identity/register.go b/generated/1.24/apis/concierge/identity/register.go new file mode 100644 index 00000000..0cefb834 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/register.go @@ -0,0 +1,38 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "identity.concierge.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind. +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns back a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &WhoAmIRequest{}, + &WhoAmIRequestList{}, + ) + return nil +} diff --git a/generated/1.24/apis/concierge/identity/types_userinfo.go b/generated/1.24/apis/concierge/identity/types_userinfo.go new file mode 100644 index 00000000..65eb50cc --- /dev/null +++ b/generated/1.24/apis/concierge/identity/types_userinfo.go @@ -0,0 +1,37 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import "fmt" + +// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it. +// Copied from the Kubernetes token review API. +type KubernetesUserInfo struct { + // User is the UserInfo associated with the current user. + User UserInfo + // Audiences are audience identifiers chosen by the authenticator. + Audiences []string +} + +// UserInfo holds the information about the user needed to implement the +// user.Info interface. +type UserInfo struct { + // The name that uniquely identifies this user among all active users. + Username string + // A unique value that identifies this user across time. If this user is + // deleted and another user by the same name is added, they will have + // different UIDs. + UID string + // The names of groups this user is a part of. + Groups []string + // Any additional information provided by the authenticator. + Extra map[string]ExtraValue +} + +// ExtraValue masks the value so protobuf can generate +type ExtraValue []string + +func (t ExtraValue) String() string { + return fmt.Sprintf("%v", []string(t)) +} diff --git a/generated/1.24/apis/concierge/identity/types_whoami.go b/generated/1.24/apis/concierge/identity/types_whoami.go new file mode 100644 index 00000000..4ba30607 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/types_whoami.go @@ -0,0 +1,40 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package identity + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// WhoAmIRequest submits a request to echo back the current authenticated user. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type WhoAmIRequest struct { + metav1.TypeMeta + metav1.ObjectMeta + + Spec WhoAmIRequestSpec + Status WhoAmIRequestStatus +} + +type WhoAmIRequestSpec struct { + // empty for now but we may add some config here in the future + // any such config must be safe in the context of an unauthenticated user +} + +type WhoAmIRequestStatus struct { + // The current authenticated user, exactly as Kubernetes understands it. + KubernetesUserInfo KubernetesUserInfo + + // We may add concierge specific information here in the future. +} + +// WhoAmIRequestList is a list of WhoAmIRequest objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type WhoAmIRequestList struct { + metav1.TypeMeta + metav1.ListMeta + + // Items is a list of WhoAmIRequest + Items []WhoAmIRequest +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/conversion.go b/generated/1.24/apis/concierge/identity/v1alpha1/conversion.go new file mode 100644 index 00000000..63ae004e --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/conversion.go @@ -0,0 +1,4 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/defaults.go b/generated/1.24/apis/concierge/identity/v1alpha1/defaults.go new file mode 100644 index 00000000..15c2bec9 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/defaults.go @@ -0,0 +1,12 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/doc.go b/generated/1.24/apis/concierge/identity/v1alpha1/doc.go new file mode 100644 index 00000000..4a368dc1 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/doc.go @@ -0,0 +1,11 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:conversion-gen=go.pinniped.dev/generated/1.24/apis/concierge/identity +// +k8s:defaulter-gen=TypeMeta +// +groupName=identity.concierge.pinniped.dev + +// Package v1alpha1 is the v1alpha1 version of the Pinniped identity API. +package v1alpha1 diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/register.go b/generated/1.24/apis/concierge/identity/v1alpha1/register.go new file mode 100644 index 00000000..0e17d584 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/register.go @@ -0,0 +1,43 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "identity.concierge.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &WhoAmIRequest{}, + &WhoAmIRequestList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/types_userinfo.go b/generated/1.24/apis/concierge/identity/v1alpha1/types_userinfo.go new file mode 100644 index 00000000..4e509fc1 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/types_userinfo.go @@ -0,0 +1,41 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import "fmt" + +// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it. +// Copied from the Kubernetes token review API. +type KubernetesUserInfo struct { + // User is the UserInfo associated with the current user. + User UserInfo `json:"user"` + // Audiences are audience identifiers chosen by the authenticator. + // +optional + Audiences []string `json:"audiences,omitempty"` +} + +// UserInfo holds the information about the user needed to implement the +// user.Info interface. +type UserInfo struct { + // The name that uniquely identifies this user among all active users. + Username string `json:"username"` + // A unique value that identifies this user across time. If this user is + // deleted and another user by the same name is added, they will have + // different UIDs. + // +optional + UID string `json:"uid,omitempty"` + // The names of groups this user is a part of. + // +optional + Groups []string `json:"groups,omitempty"` + // Any additional information provided by the authenticator. + // +optional + Extra map[string]ExtraValue `json:"extra,omitempty"` +} + +// ExtraValue masks the value so protobuf can generate +type ExtraValue []string + +func (t ExtraValue) String() string { + return fmt.Sprintf("%v", []string(t)) +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/types_whoami.go b/generated/1.24/apis/concierge/identity/v1alpha1/types_whoami.go new file mode 100644 index 00000000..3d884346 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/types_whoami.go @@ -0,0 +1,43 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// WhoAmIRequest submits a request to echo back the current authenticated user. +// +genclient +// +genclient:nonNamespaced +// +genclient:onlyVerbs=create +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type WhoAmIRequest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec WhoAmIRequestSpec `json:"spec,omitempty"` + Status WhoAmIRequestStatus `json:"status,omitempty"` +} + +type WhoAmIRequestSpec struct { + // empty for now but we may add some config here in the future + // any such config must be safe in the context of an unauthenticated user +} + +type WhoAmIRequestStatus struct { + // The current authenticated user, exactly as Kubernetes understands it. + KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"` + + // We may add concierge specific information here in the future. +} + +// WhoAmIRequestList is a list of WhoAmIRequest objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type WhoAmIRequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is a list of WhoAmIRequest + Items []WhoAmIRequest `json:"items"` +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.conversion.go b/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.conversion.go new file mode 100644 index 00000000..14dee153 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,235 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + unsafe "unsafe" + + identity "go.pinniped.dev/generated/1.24/apis/concierge/identity" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*KubernetesUserInfo)(nil), (*identity.KubernetesUserInfo)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_KubernetesUserInfo_To_identity_KubernetesUserInfo(a.(*KubernetesUserInfo), b.(*identity.KubernetesUserInfo), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*identity.KubernetesUserInfo)(nil), (*KubernetesUserInfo)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_identity_KubernetesUserInfo_To_v1alpha1_KubernetesUserInfo(a.(*identity.KubernetesUserInfo), b.(*KubernetesUserInfo), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*UserInfo)(nil), (*identity.UserInfo)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_UserInfo_To_identity_UserInfo(a.(*UserInfo), b.(*identity.UserInfo), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*identity.UserInfo)(nil), (*UserInfo)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_identity_UserInfo_To_v1alpha1_UserInfo(a.(*identity.UserInfo), b.(*UserInfo), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*WhoAmIRequest)(nil), (*identity.WhoAmIRequest)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_WhoAmIRequest_To_identity_WhoAmIRequest(a.(*WhoAmIRequest), b.(*identity.WhoAmIRequest), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*identity.WhoAmIRequest)(nil), (*WhoAmIRequest)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_identity_WhoAmIRequest_To_v1alpha1_WhoAmIRequest(a.(*identity.WhoAmIRequest), b.(*WhoAmIRequest), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*WhoAmIRequestList)(nil), (*identity.WhoAmIRequestList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_WhoAmIRequestList_To_identity_WhoAmIRequestList(a.(*WhoAmIRequestList), b.(*identity.WhoAmIRequestList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*identity.WhoAmIRequestList)(nil), (*WhoAmIRequestList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_identity_WhoAmIRequestList_To_v1alpha1_WhoAmIRequestList(a.(*identity.WhoAmIRequestList), b.(*WhoAmIRequestList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*WhoAmIRequestSpec)(nil), (*identity.WhoAmIRequestSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_WhoAmIRequestSpec_To_identity_WhoAmIRequestSpec(a.(*WhoAmIRequestSpec), b.(*identity.WhoAmIRequestSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*identity.WhoAmIRequestSpec)(nil), (*WhoAmIRequestSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_identity_WhoAmIRequestSpec_To_v1alpha1_WhoAmIRequestSpec(a.(*identity.WhoAmIRequestSpec), b.(*WhoAmIRequestSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*WhoAmIRequestStatus)(nil), (*identity.WhoAmIRequestStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_WhoAmIRequestStatus_To_identity_WhoAmIRequestStatus(a.(*WhoAmIRequestStatus), b.(*identity.WhoAmIRequestStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*identity.WhoAmIRequestStatus)(nil), (*WhoAmIRequestStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_identity_WhoAmIRequestStatus_To_v1alpha1_WhoAmIRequestStatus(a.(*identity.WhoAmIRequestStatus), b.(*WhoAmIRequestStatus), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1alpha1_KubernetesUserInfo_To_identity_KubernetesUserInfo(in *KubernetesUserInfo, out *identity.KubernetesUserInfo, s conversion.Scope) error { + if err := Convert_v1alpha1_UserInfo_To_identity_UserInfo(&in.User, &out.User, s); err != nil { + return err + } + out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences)) + return nil +} + +// Convert_v1alpha1_KubernetesUserInfo_To_identity_KubernetesUserInfo is an autogenerated conversion function. +func Convert_v1alpha1_KubernetesUserInfo_To_identity_KubernetesUserInfo(in *KubernetesUserInfo, out *identity.KubernetesUserInfo, s conversion.Scope) error { + return autoConvert_v1alpha1_KubernetesUserInfo_To_identity_KubernetesUserInfo(in, out, s) +} + +func autoConvert_identity_KubernetesUserInfo_To_v1alpha1_KubernetesUserInfo(in *identity.KubernetesUserInfo, out *KubernetesUserInfo, s conversion.Scope) error { + if err := Convert_identity_UserInfo_To_v1alpha1_UserInfo(&in.User, &out.User, s); err != nil { + return err + } + out.Audiences = *(*[]string)(unsafe.Pointer(&in.Audiences)) + return nil +} + +// Convert_identity_KubernetesUserInfo_To_v1alpha1_KubernetesUserInfo is an autogenerated conversion function. +func Convert_identity_KubernetesUserInfo_To_v1alpha1_KubernetesUserInfo(in *identity.KubernetesUserInfo, out *KubernetesUserInfo, s conversion.Scope) error { + return autoConvert_identity_KubernetesUserInfo_To_v1alpha1_KubernetesUserInfo(in, out, s) +} + +func autoConvert_v1alpha1_UserInfo_To_identity_UserInfo(in *UserInfo, out *identity.UserInfo, s conversion.Scope) error { + out.Username = in.Username + out.UID = in.UID + out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups)) + out.Extra = *(*map[string]identity.ExtraValue)(unsafe.Pointer(&in.Extra)) + return nil +} + +// Convert_v1alpha1_UserInfo_To_identity_UserInfo is an autogenerated conversion function. +func Convert_v1alpha1_UserInfo_To_identity_UserInfo(in *UserInfo, out *identity.UserInfo, s conversion.Scope) error { + return autoConvert_v1alpha1_UserInfo_To_identity_UserInfo(in, out, s) +} + +func autoConvert_identity_UserInfo_To_v1alpha1_UserInfo(in *identity.UserInfo, out *UserInfo, s conversion.Scope) error { + out.Username = in.Username + out.UID = in.UID + out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups)) + out.Extra = *(*map[string]ExtraValue)(unsafe.Pointer(&in.Extra)) + return nil +} + +// Convert_identity_UserInfo_To_v1alpha1_UserInfo is an autogenerated conversion function. +func Convert_identity_UserInfo_To_v1alpha1_UserInfo(in *identity.UserInfo, out *UserInfo, s conversion.Scope) error { + return autoConvert_identity_UserInfo_To_v1alpha1_UserInfo(in, out, s) +} + +func autoConvert_v1alpha1_WhoAmIRequest_To_identity_WhoAmIRequest(in *WhoAmIRequest, out *identity.WhoAmIRequest, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1alpha1_WhoAmIRequestSpec_To_identity_WhoAmIRequestSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v1alpha1_WhoAmIRequestStatus_To_identity_WhoAmIRequestStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_v1alpha1_WhoAmIRequest_To_identity_WhoAmIRequest is an autogenerated conversion function. +func Convert_v1alpha1_WhoAmIRequest_To_identity_WhoAmIRequest(in *WhoAmIRequest, out *identity.WhoAmIRequest, s conversion.Scope) error { + return autoConvert_v1alpha1_WhoAmIRequest_To_identity_WhoAmIRequest(in, out, s) +} + +func autoConvert_identity_WhoAmIRequest_To_v1alpha1_WhoAmIRequest(in *identity.WhoAmIRequest, out *WhoAmIRequest, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_identity_WhoAmIRequestSpec_To_v1alpha1_WhoAmIRequestSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_identity_WhoAmIRequestStatus_To_v1alpha1_WhoAmIRequestStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_identity_WhoAmIRequest_To_v1alpha1_WhoAmIRequest is an autogenerated conversion function. +func Convert_identity_WhoAmIRequest_To_v1alpha1_WhoAmIRequest(in *identity.WhoAmIRequest, out *WhoAmIRequest, s conversion.Scope) error { + return autoConvert_identity_WhoAmIRequest_To_v1alpha1_WhoAmIRequest(in, out, s) +} + +func autoConvert_v1alpha1_WhoAmIRequestList_To_identity_WhoAmIRequestList(in *WhoAmIRequestList, out *identity.WhoAmIRequestList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]identity.WhoAmIRequest)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1alpha1_WhoAmIRequestList_To_identity_WhoAmIRequestList is an autogenerated conversion function. +func Convert_v1alpha1_WhoAmIRequestList_To_identity_WhoAmIRequestList(in *WhoAmIRequestList, out *identity.WhoAmIRequestList, s conversion.Scope) error { + return autoConvert_v1alpha1_WhoAmIRequestList_To_identity_WhoAmIRequestList(in, out, s) +} + +func autoConvert_identity_WhoAmIRequestList_To_v1alpha1_WhoAmIRequestList(in *identity.WhoAmIRequestList, out *WhoAmIRequestList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]WhoAmIRequest)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_identity_WhoAmIRequestList_To_v1alpha1_WhoAmIRequestList is an autogenerated conversion function. +func Convert_identity_WhoAmIRequestList_To_v1alpha1_WhoAmIRequestList(in *identity.WhoAmIRequestList, out *WhoAmIRequestList, s conversion.Scope) error { + return autoConvert_identity_WhoAmIRequestList_To_v1alpha1_WhoAmIRequestList(in, out, s) +} + +func autoConvert_v1alpha1_WhoAmIRequestSpec_To_identity_WhoAmIRequestSpec(in *WhoAmIRequestSpec, out *identity.WhoAmIRequestSpec, s conversion.Scope) error { + return nil +} + +// Convert_v1alpha1_WhoAmIRequestSpec_To_identity_WhoAmIRequestSpec is an autogenerated conversion function. +func Convert_v1alpha1_WhoAmIRequestSpec_To_identity_WhoAmIRequestSpec(in *WhoAmIRequestSpec, out *identity.WhoAmIRequestSpec, s conversion.Scope) error { + return autoConvert_v1alpha1_WhoAmIRequestSpec_To_identity_WhoAmIRequestSpec(in, out, s) +} + +func autoConvert_identity_WhoAmIRequestSpec_To_v1alpha1_WhoAmIRequestSpec(in *identity.WhoAmIRequestSpec, out *WhoAmIRequestSpec, s conversion.Scope) error { + return nil +} + +// Convert_identity_WhoAmIRequestSpec_To_v1alpha1_WhoAmIRequestSpec is an autogenerated conversion function. +func Convert_identity_WhoAmIRequestSpec_To_v1alpha1_WhoAmIRequestSpec(in *identity.WhoAmIRequestSpec, out *WhoAmIRequestSpec, s conversion.Scope) error { + return autoConvert_identity_WhoAmIRequestSpec_To_v1alpha1_WhoAmIRequestSpec(in, out, s) +} + +func autoConvert_v1alpha1_WhoAmIRequestStatus_To_identity_WhoAmIRequestStatus(in *WhoAmIRequestStatus, out *identity.WhoAmIRequestStatus, s conversion.Scope) error { + if err := Convert_v1alpha1_KubernetesUserInfo_To_identity_KubernetesUserInfo(&in.KubernetesUserInfo, &out.KubernetesUserInfo, s); err != nil { + return err + } + return nil +} + +// Convert_v1alpha1_WhoAmIRequestStatus_To_identity_WhoAmIRequestStatus is an autogenerated conversion function. +func Convert_v1alpha1_WhoAmIRequestStatus_To_identity_WhoAmIRequestStatus(in *WhoAmIRequestStatus, out *identity.WhoAmIRequestStatus, s conversion.Scope) error { + return autoConvert_v1alpha1_WhoAmIRequestStatus_To_identity_WhoAmIRequestStatus(in, out, s) +} + +func autoConvert_identity_WhoAmIRequestStatus_To_v1alpha1_WhoAmIRequestStatus(in *identity.WhoAmIRequestStatus, out *WhoAmIRequestStatus, s conversion.Scope) error { + if err := Convert_identity_KubernetesUserInfo_To_v1alpha1_KubernetesUserInfo(&in.KubernetesUserInfo, &out.KubernetesUserInfo, s); err != nil { + return err + } + return nil +} + +// Convert_identity_WhoAmIRequestStatus_To_v1alpha1_WhoAmIRequestStatus is an autogenerated conversion function. +func Convert_identity_WhoAmIRequestStatus_To_v1alpha1_WhoAmIRequestStatus(in *identity.WhoAmIRequestStatus, out *WhoAmIRequestStatus, s conversion.Scope) error { + return autoConvert_identity_WhoAmIRequestStatus_To_v1alpha1_WhoAmIRequestStatus(in, out, s) +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..8206b6dd --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,185 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ExtraValue) DeepCopyInto(out *ExtraValue) { + { + in := &in + *out = make(ExtraValue, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraValue. +func (in ExtraValue) DeepCopy() ExtraValue { + if in == nil { + return nil + } + out := new(ExtraValue) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesUserInfo) DeepCopyInto(out *KubernetesUserInfo) { + *out = *in + in.User.DeepCopyInto(&out.User) + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesUserInfo. +func (in *KubernetesUserInfo) DeepCopy() *KubernetesUserInfo { + if in == nil { + return nil + } + out := new(KubernetesUserInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserInfo) DeepCopyInto(out *UserInfo) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Extra != nil { + in, out := &in.Extra, &out.Extra + *out = make(map[string]ExtraValue, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(ExtraValue, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserInfo. +func (in *UserInfo) DeepCopy() *UserInfo { + if in == nil { + return nil + } + out := new(UserInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequest) DeepCopyInto(out *WhoAmIRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequest. +func (in *WhoAmIRequest) DeepCopy() *WhoAmIRequest { + if in == nil { + return nil + } + out := new(WhoAmIRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WhoAmIRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequestList) DeepCopyInto(out *WhoAmIRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WhoAmIRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequestList. +func (in *WhoAmIRequestList) DeepCopy() *WhoAmIRequestList { + if in == nil { + return nil + } + out := new(WhoAmIRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WhoAmIRequestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequestSpec) DeepCopyInto(out *WhoAmIRequestSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequestSpec. +func (in *WhoAmIRequestSpec) DeepCopy() *WhoAmIRequestSpec { + if in == nil { + return nil + } + out := new(WhoAmIRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequestStatus) DeepCopyInto(out *WhoAmIRequestStatus) { + *out = *in + in.KubernetesUserInfo.DeepCopyInto(&out.KubernetesUserInfo) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequestStatus. +func (in *WhoAmIRequestStatus) DeepCopy() *WhoAmIRequestStatus { + if in == nil { + return nil + } + out := new(WhoAmIRequestStatus) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.defaults.go b/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.defaults.go new file mode 100644 index 00000000..9097a935 --- /dev/null +++ b/generated/1.24/apis/concierge/identity/v1alpha1/zz_generated.defaults.go @@ -0,0 +1,20 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/generated/1.24/apis/concierge/identity/validation/validation.go b/generated/1.24/apis/concierge/identity/validation/validation.go new file mode 100644 index 00000000..66c191df --- /dev/null +++ b/generated/1.24/apis/concierge/identity/validation/validation.go @@ -0,0 +1,14 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package validation + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + + identityapi "go.pinniped.dev/generated/1.24/apis/concierge/identity" +) + +func ValidateWhoAmIRequest(whoAmIRequest *identityapi.WhoAmIRequest) field.ErrorList { + return nil // add validation for spec here if we expand it +} diff --git a/generated/1.24/apis/concierge/identity/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/identity/zz_generated.deepcopy.go new file mode 100644 index 00000000..4d844ffd --- /dev/null +++ b/generated/1.24/apis/concierge/identity/zz_generated.deepcopy.go @@ -0,0 +1,185 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package identity + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ExtraValue) DeepCopyInto(out *ExtraValue) { + { + in := &in + *out = make(ExtraValue, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraValue. +func (in ExtraValue) DeepCopy() ExtraValue { + if in == nil { + return nil + } + out := new(ExtraValue) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesUserInfo) DeepCopyInto(out *KubernetesUserInfo) { + *out = *in + in.User.DeepCopyInto(&out.User) + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesUserInfo. +func (in *KubernetesUserInfo) DeepCopy() *KubernetesUserInfo { + if in == nil { + return nil + } + out := new(KubernetesUserInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserInfo) DeepCopyInto(out *UserInfo) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Extra != nil { + in, out := &in.Extra, &out.Extra + *out = make(map[string]ExtraValue, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(ExtraValue, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserInfo. +func (in *UserInfo) DeepCopy() *UserInfo { + if in == nil { + return nil + } + out := new(UserInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequest) DeepCopyInto(out *WhoAmIRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequest. +func (in *WhoAmIRequest) DeepCopy() *WhoAmIRequest { + if in == nil { + return nil + } + out := new(WhoAmIRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WhoAmIRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequestList) DeepCopyInto(out *WhoAmIRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WhoAmIRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequestList. +func (in *WhoAmIRequestList) DeepCopy() *WhoAmIRequestList { + if in == nil { + return nil + } + out := new(WhoAmIRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WhoAmIRequestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequestSpec) DeepCopyInto(out *WhoAmIRequestSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequestSpec. +func (in *WhoAmIRequestSpec) DeepCopy() *WhoAmIRequestSpec { + if in == nil { + return nil + } + out := new(WhoAmIRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WhoAmIRequestStatus) DeepCopyInto(out *WhoAmIRequestStatus) { + *out = *in + in.KubernetesUserInfo.DeepCopyInto(&out.KubernetesUserInfo) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WhoAmIRequestStatus. +func (in *WhoAmIRequestStatus) DeepCopy() *WhoAmIRequestStatus { + if in == nil { + return nil + } + out := new(WhoAmIRequestStatus) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/concierge/login/doc.go b/generated/1.24/apis/concierge/login/doc.go new file mode 100644 index 00000000..14af63a2 --- /dev/null +++ b/generated/1.24/apis/concierge/login/doc.go @@ -0,0 +1,8 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:deepcopy-gen=package +// +groupName=login.concierge.pinniped.dev + +// Package login is the internal version of the Pinniped login API. +package login diff --git a/generated/1.24/apis/concierge/login/register.go b/generated/1.24/apis/concierge/login/register.go new file mode 100644 index 00000000..1c3ba130 --- /dev/null +++ b/generated/1.24/apis/concierge/login/register.go @@ -0,0 +1,38 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "login.concierge.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind. +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns back a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &TokenCredentialRequest{}, + &TokenCredentialRequestList{}, + ) + return nil +} diff --git a/generated/1.24/apis/concierge/login/types_clustercred.go b/generated/1.24/apis/concierge/login/types_clustercred.go new file mode 100644 index 00000000..dd683b81 --- /dev/null +++ b/generated/1.24/apis/concierge/login/types_clustercred.go @@ -0,0 +1,21 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ClusterCredential is a credential (token or certificate) which is valid on the Kubernetes cluster. +type ClusterCredential struct { + // ExpirationTimestamp indicates a time when the provided credentials expire. + ExpirationTimestamp metav1.Time + + // Token is a bearer token used by the client for request authentication. + Token string + + // PEM-encoded client TLS certificates (including intermediates, if any). + ClientCertificateData string + + // PEM-encoded private key for the above certificate. + ClientKeyData string +} diff --git a/generated/1.24/apis/concierge/login/types_token.go b/generated/1.24/apis/concierge/login/types_token.go new file mode 100644 index 00000000..e3af529e --- /dev/null +++ b/generated/1.24/apis/concierge/login/types_token.go @@ -0,0 +1,47 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package login + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type TokenCredentialRequestSpec struct { + // Bearer token supplied with the credential request. + Token string + + // Reference to an authenticator which can validate this credential request. + Authenticator corev1.TypedLocalObjectReference +} + +type TokenCredentialRequestStatus struct { + // A ClusterCredential will be returned for a successful credential request. + // +optional + Credential *ClusterCredential + + // An error message will be returned for an unsuccessful credential request. + // +optional + Message *string +} + +// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TokenCredentialRequest struct { + metav1.TypeMeta + metav1.ObjectMeta + + Spec TokenCredentialRequestSpec + Status TokenCredentialRequestStatus +} + +// TokenCredentialRequestList is a list of TokenCredentialRequest objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TokenCredentialRequestList struct { + metav1.TypeMeta + metav1.ListMeta + + // Items is a list of TokenCredentialRequest + Items []TokenCredentialRequest +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/conversion.go b/generated/1.24/apis/concierge/login/v1alpha1/conversion.go new file mode 100644 index 00000000..dca8bca2 --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/conversion.go @@ -0,0 +1,4 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 diff --git a/generated/1.24/apis/concierge/login/v1alpha1/defaults.go b/generated/1.24/apis/concierge/login/v1alpha1/defaults.go new file mode 100644 index 00000000..12f0acd2 --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/defaults.go @@ -0,0 +1,12 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/doc.go b/generated/1.24/apis/concierge/login/v1alpha1/doc.go new file mode 100644 index 00000000..773e6e0c --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/doc.go @@ -0,0 +1,11 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:conversion-gen=go.pinniped.dev/generated/1.24/apis/concierge/login +// +k8s:defaulter-gen=TypeMeta +// +groupName=login.concierge.pinniped.dev + +// Package v1alpha1 is the v1alpha1 version of the Pinniped login API. +package v1alpha1 diff --git a/generated/1.24/apis/concierge/login/v1alpha1/register.go b/generated/1.24/apis/concierge/login/v1alpha1/register.go new file mode 100644 index 00000000..60efec82 --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/register.go @@ -0,0 +1,43 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "login.concierge.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &TokenCredentialRequest{}, + &TokenCredentialRequestList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/types_clustercred.go b/generated/1.24/apis/concierge/login/v1alpha1/types_clustercred.go new file mode 100644 index 00000000..eb50c7fe --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/types_clustercred.go @@ -0,0 +1,22 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ClusterCredential is the cluster-specific credential returned on a successful credential request. It +// contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster. +type ClusterCredential struct { + // ExpirationTimestamp indicates a time when the provided credentials expire. + ExpirationTimestamp metav1.Time `json:"expirationTimestamp,omitempty"` + + // Token is a bearer token used by the client for request authentication. + Token string `json:"token,omitempty"` + + // PEM-encoded client TLS certificates (including intermediates, if any). + ClientCertificateData string `json:"clientCertificateData,omitempty"` + + // PEM-encoded private key for the above certificate. + ClientKeyData string `json:"clientKeyData,omitempty"` +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/types_token.go b/generated/1.24/apis/concierge/login/v1alpha1/types_token.go new file mode 100644 index 00000000..c8f3b66e --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/types_token.go @@ -0,0 +1,51 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TokenCredentialRequestSpec is the specification of a TokenCredentialRequest, expected on requests to the Pinniped API. +type TokenCredentialRequestSpec struct { + // Bearer token supplied with the credential request. + Token string `json:"token,omitempty"` + + // Reference to an authenticator which can validate this credential request. + Authenticator corev1.TypedLocalObjectReference `json:"authenticator"` +} + +// TokenCredentialRequestStatus is the status of a TokenCredentialRequest, returned on responses to the Pinniped API. +type TokenCredentialRequestStatus struct { + // A Credential will be returned for a successful credential request. + // +optional + Credential *ClusterCredential `json:"credential,omitempty"` + + // An error message will be returned for an unsuccessful credential request. + // +optional + Message *string `json:"message,omitempty"` +} + +// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential. +// +genclient +// +genclient:nonNamespaced +// +genclient:onlyVerbs=create +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TokenCredentialRequest struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TokenCredentialRequestSpec `json:"spec,omitempty"` + Status TokenCredentialRequestStatus `json:"status,omitempty"` +} + +// TokenCredentialRequestList is a list of TokenCredentialRequest objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TokenCredentialRequestList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []TokenCredentialRequest `json:"items"` +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.conversion.go b/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.conversion.go new file mode 100644 index 00000000..2afd850f --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.conversion.go @@ -0,0 +1,201 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + unsafe "unsafe" + + login "go.pinniped.dev/generated/1.24/apis/concierge/login" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*ClusterCredential)(nil), (*login.ClusterCredential)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ClusterCredential_To_login_ClusterCredential(a.(*ClusterCredential), b.(*login.ClusterCredential), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*login.ClusterCredential)(nil), (*ClusterCredential)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_login_ClusterCredential_To_v1alpha1_ClusterCredential(a.(*login.ClusterCredential), b.(*ClusterCredential), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TokenCredentialRequest)(nil), (*login.TokenCredentialRequest)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TokenCredentialRequest_To_login_TokenCredentialRequest(a.(*TokenCredentialRequest), b.(*login.TokenCredentialRequest), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*login.TokenCredentialRequest)(nil), (*TokenCredentialRequest)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_login_TokenCredentialRequest_To_v1alpha1_TokenCredentialRequest(a.(*login.TokenCredentialRequest), b.(*TokenCredentialRequest), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TokenCredentialRequestList)(nil), (*login.TokenCredentialRequestList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TokenCredentialRequestList_To_login_TokenCredentialRequestList(a.(*TokenCredentialRequestList), b.(*login.TokenCredentialRequestList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*login.TokenCredentialRequestList)(nil), (*TokenCredentialRequestList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_login_TokenCredentialRequestList_To_v1alpha1_TokenCredentialRequestList(a.(*login.TokenCredentialRequestList), b.(*TokenCredentialRequestList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TokenCredentialRequestSpec)(nil), (*login.TokenCredentialRequestSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TokenCredentialRequestSpec_To_login_TokenCredentialRequestSpec(a.(*TokenCredentialRequestSpec), b.(*login.TokenCredentialRequestSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*login.TokenCredentialRequestSpec)(nil), (*TokenCredentialRequestSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_login_TokenCredentialRequestSpec_To_v1alpha1_TokenCredentialRequestSpec(a.(*login.TokenCredentialRequestSpec), b.(*TokenCredentialRequestSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TokenCredentialRequestStatus)(nil), (*login.TokenCredentialRequestStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_TokenCredentialRequestStatus_To_login_TokenCredentialRequestStatus(a.(*TokenCredentialRequestStatus), b.(*login.TokenCredentialRequestStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*login.TokenCredentialRequestStatus)(nil), (*TokenCredentialRequestStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_login_TokenCredentialRequestStatus_To_v1alpha1_TokenCredentialRequestStatus(a.(*login.TokenCredentialRequestStatus), b.(*TokenCredentialRequestStatus), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1alpha1_ClusterCredential_To_login_ClusterCredential(in *ClusterCredential, out *login.ClusterCredential, s conversion.Scope) error { + out.ExpirationTimestamp = in.ExpirationTimestamp + out.Token = in.Token + out.ClientCertificateData = in.ClientCertificateData + out.ClientKeyData = in.ClientKeyData + return nil +} + +// Convert_v1alpha1_ClusterCredential_To_login_ClusterCredential is an autogenerated conversion function. +func Convert_v1alpha1_ClusterCredential_To_login_ClusterCredential(in *ClusterCredential, out *login.ClusterCredential, s conversion.Scope) error { + return autoConvert_v1alpha1_ClusterCredential_To_login_ClusterCredential(in, out, s) +} + +func autoConvert_login_ClusterCredential_To_v1alpha1_ClusterCredential(in *login.ClusterCredential, out *ClusterCredential, s conversion.Scope) error { + out.ExpirationTimestamp = in.ExpirationTimestamp + out.Token = in.Token + out.ClientCertificateData = in.ClientCertificateData + out.ClientKeyData = in.ClientKeyData + return nil +} + +// Convert_login_ClusterCredential_To_v1alpha1_ClusterCredential is an autogenerated conversion function. +func Convert_login_ClusterCredential_To_v1alpha1_ClusterCredential(in *login.ClusterCredential, out *ClusterCredential, s conversion.Scope) error { + return autoConvert_login_ClusterCredential_To_v1alpha1_ClusterCredential(in, out, s) +} + +func autoConvert_v1alpha1_TokenCredentialRequest_To_login_TokenCredentialRequest(in *TokenCredentialRequest, out *login.TokenCredentialRequest, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1alpha1_TokenCredentialRequestSpec_To_login_TokenCredentialRequestSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_v1alpha1_TokenCredentialRequestStatus_To_login_TokenCredentialRequestStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_v1alpha1_TokenCredentialRequest_To_login_TokenCredentialRequest is an autogenerated conversion function. +func Convert_v1alpha1_TokenCredentialRequest_To_login_TokenCredentialRequest(in *TokenCredentialRequest, out *login.TokenCredentialRequest, s conversion.Scope) error { + return autoConvert_v1alpha1_TokenCredentialRequest_To_login_TokenCredentialRequest(in, out, s) +} + +func autoConvert_login_TokenCredentialRequest_To_v1alpha1_TokenCredentialRequest(in *login.TokenCredentialRequest, out *TokenCredentialRequest, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_login_TokenCredentialRequestSpec_To_v1alpha1_TokenCredentialRequestSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := Convert_login_TokenCredentialRequestStatus_To_v1alpha1_TokenCredentialRequestStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +// Convert_login_TokenCredentialRequest_To_v1alpha1_TokenCredentialRequest is an autogenerated conversion function. +func Convert_login_TokenCredentialRequest_To_v1alpha1_TokenCredentialRequest(in *login.TokenCredentialRequest, out *TokenCredentialRequest, s conversion.Scope) error { + return autoConvert_login_TokenCredentialRequest_To_v1alpha1_TokenCredentialRequest(in, out, s) +} + +func autoConvert_v1alpha1_TokenCredentialRequestList_To_login_TokenCredentialRequestList(in *TokenCredentialRequestList, out *login.TokenCredentialRequestList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]login.TokenCredentialRequest)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1alpha1_TokenCredentialRequestList_To_login_TokenCredentialRequestList is an autogenerated conversion function. +func Convert_v1alpha1_TokenCredentialRequestList_To_login_TokenCredentialRequestList(in *TokenCredentialRequestList, out *login.TokenCredentialRequestList, s conversion.Scope) error { + return autoConvert_v1alpha1_TokenCredentialRequestList_To_login_TokenCredentialRequestList(in, out, s) +} + +func autoConvert_login_TokenCredentialRequestList_To_v1alpha1_TokenCredentialRequestList(in *login.TokenCredentialRequestList, out *TokenCredentialRequestList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]TokenCredentialRequest)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_login_TokenCredentialRequestList_To_v1alpha1_TokenCredentialRequestList is an autogenerated conversion function. +func Convert_login_TokenCredentialRequestList_To_v1alpha1_TokenCredentialRequestList(in *login.TokenCredentialRequestList, out *TokenCredentialRequestList, s conversion.Scope) error { + return autoConvert_login_TokenCredentialRequestList_To_v1alpha1_TokenCredentialRequestList(in, out, s) +} + +func autoConvert_v1alpha1_TokenCredentialRequestSpec_To_login_TokenCredentialRequestSpec(in *TokenCredentialRequestSpec, out *login.TokenCredentialRequestSpec, s conversion.Scope) error { + out.Token = in.Token + out.Authenticator = in.Authenticator + return nil +} + +// Convert_v1alpha1_TokenCredentialRequestSpec_To_login_TokenCredentialRequestSpec is an autogenerated conversion function. +func Convert_v1alpha1_TokenCredentialRequestSpec_To_login_TokenCredentialRequestSpec(in *TokenCredentialRequestSpec, out *login.TokenCredentialRequestSpec, s conversion.Scope) error { + return autoConvert_v1alpha1_TokenCredentialRequestSpec_To_login_TokenCredentialRequestSpec(in, out, s) +} + +func autoConvert_login_TokenCredentialRequestSpec_To_v1alpha1_TokenCredentialRequestSpec(in *login.TokenCredentialRequestSpec, out *TokenCredentialRequestSpec, s conversion.Scope) error { + out.Token = in.Token + out.Authenticator = in.Authenticator + return nil +} + +// Convert_login_TokenCredentialRequestSpec_To_v1alpha1_TokenCredentialRequestSpec is an autogenerated conversion function. +func Convert_login_TokenCredentialRequestSpec_To_v1alpha1_TokenCredentialRequestSpec(in *login.TokenCredentialRequestSpec, out *TokenCredentialRequestSpec, s conversion.Scope) error { + return autoConvert_login_TokenCredentialRequestSpec_To_v1alpha1_TokenCredentialRequestSpec(in, out, s) +} + +func autoConvert_v1alpha1_TokenCredentialRequestStatus_To_login_TokenCredentialRequestStatus(in *TokenCredentialRequestStatus, out *login.TokenCredentialRequestStatus, s conversion.Scope) error { + out.Credential = (*login.ClusterCredential)(unsafe.Pointer(in.Credential)) + out.Message = (*string)(unsafe.Pointer(in.Message)) + return nil +} + +// Convert_v1alpha1_TokenCredentialRequestStatus_To_login_TokenCredentialRequestStatus is an autogenerated conversion function. +func Convert_v1alpha1_TokenCredentialRequestStatus_To_login_TokenCredentialRequestStatus(in *TokenCredentialRequestStatus, out *login.TokenCredentialRequestStatus, s conversion.Scope) error { + return autoConvert_v1alpha1_TokenCredentialRequestStatus_To_login_TokenCredentialRequestStatus(in, out, s) +} + +func autoConvert_login_TokenCredentialRequestStatus_To_v1alpha1_TokenCredentialRequestStatus(in *login.TokenCredentialRequestStatus, out *TokenCredentialRequestStatus, s conversion.Scope) error { + out.Credential = (*ClusterCredential)(unsafe.Pointer(in.Credential)) + out.Message = (*string)(unsafe.Pointer(in.Message)) + return nil +} + +// Convert_login_TokenCredentialRequestStatus_To_v1alpha1_TokenCredentialRequestStatus is an autogenerated conversion function. +func Convert_login_TokenCredentialRequestStatus_To_v1alpha1_TokenCredentialRequestStatus(in *login.TokenCredentialRequestStatus, out *TokenCredentialRequestStatus, s conversion.Scope) error { + return autoConvert_login_TokenCredentialRequestStatus_To_v1alpha1_TokenCredentialRequestStatus(in, out, s) +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..6f39cfec --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,134 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterCredential) DeepCopyInto(out *ClusterCredential) { + *out = *in + in.ExpirationTimestamp.DeepCopyInto(&out.ExpirationTimestamp) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCredential. +func (in *ClusterCredential) DeepCopy() *ClusterCredential { + if in == nil { + return nil + } + out := new(ClusterCredential) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequest) DeepCopyInto(out *TokenCredentialRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequest. +func (in *TokenCredentialRequest) DeepCopy() *TokenCredentialRequest { + if in == nil { + return nil + } + out := new(TokenCredentialRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TokenCredentialRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestList) DeepCopyInto(out *TokenCredentialRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TokenCredentialRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestList. +func (in *TokenCredentialRequestList) DeepCopy() *TokenCredentialRequestList { + if in == nil { + return nil + } + out := new(TokenCredentialRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TokenCredentialRequestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestSpec) DeepCopyInto(out *TokenCredentialRequestSpec) { + *out = *in + in.Authenticator.DeepCopyInto(&out.Authenticator) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestSpec. +func (in *TokenCredentialRequestSpec) DeepCopy() *TokenCredentialRequestSpec { + if in == nil { + return nil + } + out := new(TokenCredentialRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestStatus) DeepCopyInto(out *TokenCredentialRequestStatus) { + *out = *in + if in.Credential != nil { + in, out := &in.Credential, &out.Credential + *out = new(ClusterCredential) + (*in).DeepCopyInto(*out) + } + if in.Message != nil { + in, out := &in.Message, &out.Message + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestStatus. +func (in *TokenCredentialRequestStatus) DeepCopy() *TokenCredentialRequestStatus { + if in == nil { + return nil + } + out := new(TokenCredentialRequestStatus) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.defaults.go b/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.defaults.go new file mode 100644 index 00000000..9097a935 --- /dev/null +++ b/generated/1.24/apis/concierge/login/v1alpha1/zz_generated.defaults.go @@ -0,0 +1,20 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/generated/1.24/apis/concierge/login/zz_generated.deepcopy.go b/generated/1.24/apis/concierge/login/zz_generated.deepcopy.go new file mode 100644 index 00000000..07f4f074 --- /dev/null +++ b/generated/1.24/apis/concierge/login/zz_generated.deepcopy.go @@ -0,0 +1,134 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package login + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterCredential) DeepCopyInto(out *ClusterCredential) { + *out = *in + in.ExpirationTimestamp.DeepCopyInto(&out.ExpirationTimestamp) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCredential. +func (in *ClusterCredential) DeepCopy() *ClusterCredential { + if in == nil { + return nil + } + out := new(ClusterCredential) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequest) DeepCopyInto(out *TokenCredentialRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequest. +func (in *TokenCredentialRequest) DeepCopy() *TokenCredentialRequest { + if in == nil { + return nil + } + out := new(TokenCredentialRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TokenCredentialRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestList) DeepCopyInto(out *TokenCredentialRequestList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TokenCredentialRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestList. +func (in *TokenCredentialRequestList) DeepCopy() *TokenCredentialRequestList { + if in == nil { + return nil + } + out := new(TokenCredentialRequestList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TokenCredentialRequestList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestSpec) DeepCopyInto(out *TokenCredentialRequestSpec) { + *out = *in + in.Authenticator.DeepCopyInto(&out.Authenticator) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestSpec. +func (in *TokenCredentialRequestSpec) DeepCopy() *TokenCredentialRequestSpec { + if in == nil { + return nil + } + out := new(TokenCredentialRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenCredentialRequestStatus) DeepCopyInto(out *TokenCredentialRequestStatus) { + *out = *in + if in.Credential != nil { + in, out := &in.Credential, &out.Credential + *out = new(ClusterCredential) + (*in).DeepCopyInto(*out) + } + if in.Message != nil { + in, out := &in.Message, &out.Message + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenCredentialRequestStatus. +func (in *TokenCredentialRequestStatus) DeepCopy() *TokenCredentialRequestStatus { + if in == nil { + return nil + } + out := new(TokenCredentialRequestStatus) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/go.mod b/generated/1.24/apis/go.mod new file mode 100644 index 00000000..e456bb03 --- /dev/null +++ b/generated/1.24/apis/go.mod @@ -0,0 +1,9 @@ +// This go.mod file is generated by ./hack/codegen.sh. +module go.pinniped.dev/generated/1.24/apis + +go 1.13 + +require ( + k8s.io/api v0.24.1 + k8s.io/apimachinery v0.24.1 +) diff --git a/generated/1.24/apis/go.sum b/generated/1.24/apis/go.sum new file mode 100644 index 00000000..0e88f9a6 --- /dev/null +++ b/generated/1.24/apis/go.sum @@ -0,0 +1,241 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.24.1 h1:BjCMRDcyEYz03joa3K1+rbshwh1Ay6oB53+iUx2H8UY= +k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= +k8s.io/apimachinery v0.24.1 h1:ShD4aDxTQKN5zNf8K1RQ2u98ELLdIW7jEnlO9uAMX/I= +k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +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.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/doc.go b/generated/1.24/apis/supervisor/config/v1alpha1/doc.go new file mode 100644 index 00000000..465ebd73 --- /dev/null +++ b/generated/1.24/apis/supervisor/config/v1alpha1/doc.go @@ -0,0 +1,11 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:conversion-gen=go.pinniped.dev/generated/1.24/apis/supervisor/config +// +k8s:defaulter-gen=TypeMeta +// +groupName=config.supervisor.pinniped.dev + +// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuration API. +package v1alpha1 diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/register.go b/generated/1.24/apis/supervisor/config/v1alpha1/register.go new file mode 100644 index 00000000..69045298 --- /dev/null +++ b/generated/1.24/apis/supervisor/config/v1alpha1/register.go @@ -0,0 +1,43 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "config.supervisor.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &FederationDomain{}, + &FederationDomainList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go new file mode 100644 index 00000000..27de4401 --- /dev/null +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_federationdomain.go @@ -0,0 +1,135 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret +type FederationDomainStatusCondition string + +const ( + SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success") + DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate") + SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret") + InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid") +) + +// FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider. +type FederationDomainTLSSpec struct { + // SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains + // the TLS serving certificate for the HTTPS endpoints served by this FederationDomain. When provided, the TLS Secret + // named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use + // for TLS. + // + // Server Name Indication (SNI) is an extension to the Transport Layer Security (TLS) supported by all major browsers. + // + // SecretName is required if you would like to use different TLS certificates for issuers of different hostnames. + // SNI requests do not include port numbers, so all issuers with the same DNS hostname must use the same + // SecretName value even if they have different port numbers. + // + // SecretName is not required when you would like to use only the HTTP endpoints (e.g. when the HTTP listener is + // configured to listen on loopback interfaces or UNIX domain sockets for traffic from a service mesh sidecar). + // It is also not required when you would like all requests to this OIDC Provider's HTTPS endpoints to + // use the default TLS certificate, which is configured elsewhere. + // + // When your Issuer URL's host is an IP address, then this field is ignored. SNI does not work for IP addresses. + // + // +optional + SecretName string `json:"secretName,omitempty"` +} + +// FederationDomainSpec is a struct that describes an OIDC Provider. +type FederationDomainSpec struct { + // Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the + // identifier that it will use for the iss claim in issued JWTs. This field will also be used as + // the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is + // https://example.com/foo, then your authorization endpoint will look like + // https://example.com/foo/some/path/to/auth/endpoint). + // + // See + // https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information. + // +kubebuilder:validation:MinLength=1 + Issuer string `json:"issuer"` + + // TLS configures how this FederationDomain is served over Transport Layer Security (TLS). + // +optional + TLS *FederationDomainTLSSpec `json:"tls,omitempty"` +} + +// FederationDomainSecrets holds information about this OIDC Provider's secrets. +type FederationDomainSecrets struct { + // JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are + // stored. If it is empty, then the signing/verification keys are either unknown or they don't + // exist. + // +optional + JWKS corev1.LocalObjectReference `json:"jwks,omitempty"` + + // TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing tokens is stored. + // +optional + TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // signing state parameters is stored. + // +optional + StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"` + + // StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for + // encrypting state parameters is stored. + // +optional + StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"` +} + +// FederationDomainStatus is a struct that describes the actual state of an OIDC Provider. +type FederationDomainStatus struct { + // Status holds an enum that describes the state of this OIDC Provider. Note that this Status can + // represent success or failure. + // +optional + Status FederationDomainStatusCondition `json:"status,omitempty"` + + // Message provides human-readable details about the Status. + // +optional + Message string `json:"message,omitempty"` + + // LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get + // around some undesirable behavior with respect to the empty metav1.Time value (see + // https://github.com/kubernetes/kubernetes/issues/86811). + // +optional + LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + + // Secrets contains information about this OIDC Provider's secrets. + // +optional + Secrets FederationDomainSecrets `json:"secrets,omitempty"` +} + +// FederationDomain describes the configuration of an OIDC provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped +// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type FederationDomain struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec of the OIDC provider. + Spec FederationDomainSpec `json:"spec"` + + // Status of the OIDC provider. + Status FederationDomainStatus `json:"status,omitempty"` +} + +// List of FederationDomain objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type FederationDomainList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []FederationDomain `json:"items"` +} diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..856b8988 --- /dev/null +++ b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,152 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomain. +func (in *FederationDomain) DeepCopy() *FederationDomain { + if in == nil { + return nil + } + out := new(FederationDomain) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FederationDomain) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainList) DeepCopyInto(out *FederationDomainList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FederationDomain, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainList. +func (in *FederationDomainList) DeepCopy() *FederationDomainList { + if in == nil { + return nil + } + out := new(FederationDomainList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FederationDomainList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainSecrets) DeepCopyInto(out *FederationDomainSecrets) { + *out = *in + out.JWKS = in.JWKS + out.TokenSigningKey = in.TokenSigningKey + out.StateSigningKey = in.StateSigningKey + out.StateEncryptionKey = in.StateEncryptionKey + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainSecrets. +func (in *FederationDomainSecrets) DeepCopy() *FederationDomainSecrets { + if in == nil { + return nil + } + out := new(FederationDomainSecrets) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainSpec) DeepCopyInto(out *FederationDomainSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(FederationDomainTLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainSpec. +func (in *FederationDomainSpec) DeepCopy() *FederationDomainSpec { + if in == nil { + return nil + } + out := new(FederationDomainSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainStatus) DeepCopyInto(out *FederationDomainStatus) { + *out = *in + if in.LastUpdateTime != nil { + in, out := &in.LastUpdateTime, &out.LastUpdateTime + *out = (*in).DeepCopy() + } + out.Secrets = in.Secrets + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainStatus. +func (in *FederationDomainStatus) DeepCopy() *FederationDomainStatus { + if in == nil { + return nil + } + out := new(FederationDomainStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FederationDomainTLSSpec) DeepCopyInto(out *FederationDomainTLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FederationDomainTLSSpec. +func (in *FederationDomainTLSSpec) DeepCopy() *FederationDomainTLSSpec { + if in == nil { + return nil + } + out := new(FederationDomainTLSSpec) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/doc.go b/generated/1.24/apis/supervisor/idp/v1alpha1/doc.go new file mode 100644 index 00000000..a9e91c47 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/doc.go @@ -0,0 +1,11 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:defaulter-gen=TypeMeta +// +groupName=idp.supervisor.pinniped.dev +// +groupGoName=IDP + +// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity provider (IDP) API. +package v1alpha1 diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/register.go b/generated/1.24/apis/supervisor/idp/v1alpha1/register.go new file mode 100644 index 00000000..8829a863 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/register.go @@ -0,0 +1,47 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "idp.supervisor.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &OIDCIdentityProvider{}, + &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go new file mode 100644 index 00000000..18626629 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -0,0 +1,207 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +type ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full dn (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once and will be dynamically replaced by the username for which the search is being run. + // E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see + // https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory filters, see + // https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. + // Optional. When not specified, the default will act as if the filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` + + // The user's group membership is refreshed as they interact with the supervisor + // to obtain new credentials (as their old credentials expire). This allows group + // membership changes to be quickly reflected into Kubernetes clusters. Since + // group membership is often used to bind authorization policies, it is important + // to keep the groups observed in Kubernetes clusters in-sync with the identity + // provider. + // + // In some environments, frequent group membership queries may result in a + // significant performance impact on the identity provider and/or the supervisor. + // The best approach to handle performance impacts is to tweak the group query + // to be more performant, for example by disabling nested group search or by + // using a more targeted group search base. + // + // If the group search query cannot be made performant and you are willing to + // have group memberships remain static for approximately a day, then set + // skipGroupRefresh to true. This is an insecure configuration as authorization + // policies that are bound to group membership will not notice if a user has + // been removed from a particular group until their next login. + // + // This is an experimental feature that may be removed or significantly altered + // in the future. Consumers of this configuration should carefully read all + // release notes before upgrading to ensure that the meaning of this field has + // not changed. + SkipGroupRefresh bool `json:"skipGroupRefresh,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go new file mode 100644 index 00000000..09cb843d --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -0,0 +1,196 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +type LDAPIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full dn (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the LDAP entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default + // value of "dn={}" would not work. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". + // +kubebuilder:validation:MinLength=1 + UID string `json:"uid,omitempty"` +} + +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type LDAPIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once and will be dynamically replaced by the username for which the search is being run. + // E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see + // https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type LDAPIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about LDAP filters, see + // https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. + // Optional. When not specified, the default will act as if the Filter were specified as "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` + + // The user's group membership is refreshed as they interact with the supervisor + // to obtain new credentials (as their old credentials expire). This allows group + // membership changes to be quickly reflected into Kubernetes clusters. Since + // group membership is often used to bind authorization policies, it is important + // to keep the groups observed in Kubernetes clusters in-sync with the identity + // provider. + // + // In some environments, frequent group membership queries may result in a + // significant performance impact on the identity provider and/or the supervisor. + // The best approach to handle performance impacts is to tweak the group query + // to be more performant, for example by disabling nested group search or by + // using a more targeted group search base. + // + // If the group search query cannot be made performant and you are willing to + // have group memberships remain static for approximately a day, then set + // skipGroupRefresh to true. This is an insecure configuration as authorization + // policies that are bound to group membership will not notice if a user has + // been removed from a particular group until their next login. + // + // This is an experimental feature that may be removed or significantly altered + // in the future. Consumers of this configuration should carefully read all + // release notes before upgrading to ensure that the meaning of this field has + // not changed. + SkipGroupRefresh bool `json:"skipGroupRefresh,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_meta.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_meta.go new file mode 100644 index 00000000..76a7d547 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go new file mode 100644 index 00000000..798275a9 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go @@ -0,0 +1,206 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type OIDCIdentityProviderPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCIdentityProvider resources. + PhasePending OIDCIdentityProviderPhase = "Pending" + + // PhaseReady is the phase for an OIDCIdentityProvider resource in a healthy state. + PhaseReady OIDCIdentityProviderPhase = "Ready" + + // PhaseError is the phase for an OIDCIdentityProvider in an unhealthy state. + PhaseError OIDCIdentityProviderPhase = "Error" +) + +// OIDCIdentityProviderStatus is the status of an OIDC identity provider. +type OIDCIdentityProviderStatus struct { + // Phase summarizes the overall status of the OIDCIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization +// request parameters. +type OIDCAuthorizationConfig struct { + // additionalScopes are the additional scopes that will be requested from your OIDC provider in the authorization + // request during an OIDC Authorization Code Flow and in the token request during a Resource Owner Password Credentials + // Grant. Note that the "openid" scope will always be requested regardless of the value in this setting, since it is + // always required according to the OIDC spec. By default, when this field is not set, the Supervisor will request + // the following scopes: "openid", "offline_access", "email", and "profile". See + // https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims for a description of the "profile" and "email" + // scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess for a description of the + // "offline_access" scope. This default value may change in future versions of Pinniped as the standard evolves, + // or as common patterns used by providers who implement the standard in the ecosystem evolve. + // By setting this list to anything other than an empty list, you are overriding the + // default value, so you may wish to include some of "offline_access", "email", and "profile" in your override list. + // If you do not want any of these scopes to be requested, you may set this list to contain only "openid". + // Some OIDC providers may also require a scope to get access to the user's group membership, in which case you + // may wish to include it in this list. Sometimes the scope to request the user's group membership is called + // "groups", but unfortunately this is not specified in the OIDC standard. + // Generally speaking, you should include any scopes required to cause the appropriate claims to be the returned by + // your OIDC provider in the ID token or userinfo endpoint results for those claims which you would like to use in + // the oidcClaims settings to determine the usernames and group memberships of your Kubernetes users. See + // your OIDC provider's documentation for more information about what scopes are available to request claims. + // Additionally, the Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the Supervisor + // from these authorization flows. For most OIDC providers, the scope required to receive refresh tokens will be + // "offline_access". See the documentation of your OIDC provider's authorization and token endpoints for its + // requirements for what to include in the request in order to receive a refresh token in the response, if anything. + // Note that it may be safe to send "offline_access" even to providers which do not require it, since the provider + // may ignore scopes that it does not understand or require (see + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). In the unusual case that you must avoid sending the + // "offline_access" scope, then you must override the default value of this setting. This is required if your OIDC + // provider will reject the request when it includes "offline_access" (e.g. GitLab's OIDC provider). + // +optional + AdditionalScopes []string `json:"additionalScopes,omitempty"` + + // additionalAuthorizeParameters are extra query parameters that should be included in the authorize request to your + // OIDC provider in the authorization request during an OIDC Authorization Code Flow. By default, no extra + // parameters are sent. The standard parameters that will be sent are "response_type", "scope", "client_id", + // "state", "nonce", "code_challenge", "code_challenge_method", and "redirect_uri". These parameters cannot be + // included in this setting. Additionally, the "hd" parameter cannot be included in this setting at this time. + // The "hd" parameter is used by Google's OIDC provider to provide a hint as to which "hosted domain" the user + // should use during login. However, Pinniped does not yet support validating the hosted domain in the resulting + // ID token, so it is not yet safe to use this feature of Google's OIDC provider with Pinniped. + // This setting does not influence the parameters sent to the token endpoint in the Resource Owner Password + // Credentials Grant. The Pinniped Supervisor requires that your OIDC provider returns refresh tokens to the + // Supervisor from the authorization flows. Some OIDC providers may require a certain value for the "prompt" + // parameter in order to properly request refresh tokens. See the documentation of your OIDC provider's + // authorization endpoint for its requirements for what to include in the request in order to receive a refresh + // token in the response, if anything. If your provider requires the prompt parameter to request a refresh token, + // then include it here. Also note that most providers also require a certain scope to be requested in order to + // receive refresh tokens. See the additionalScopes setting for more information about using scopes to request + // refresh tokens. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + // +listType=map + // +listMapKey=name + AdditionalAuthorizeParameters []Parameter `json:"additionalAuthorizeParameters,omitempty"` + + // allowPasswordGrant, when true, will allow the use of OAuth 2.0's Resource Owner Password Credentials Grant + // (see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to authenticate to the OIDC provider using a + // username and password without a web browser, in addition to the usual browser-based OIDC Authorization Code Flow. + // The Resource Owner Password Credentials Grant is not officially part of the OIDC specification, so it may not be + // supported by your OIDC provider. If your OIDC provider supports returning ID tokens from a Resource Owner Password + // Credentials Grant token request, then you can choose to set this field to true. This will allow end users to choose + // to present their username and password to the kubectl CLI (using the Pinniped plugin) to authenticate to the + // cluster, without using a web browser to log in as is customary in OIDC Authorization Code Flow. This may be + // convenient for users, especially for identities from your OIDC provider which are not intended to represent a human + // actor, such as service accounts performing actions in a CI/CD environment. Even if your OIDC provider supports it, + // you may wish to disable this behavior by setting this field to false when you prefer to only allow users of this + // OIDCIdentityProvider to log in via the browser-based OIDC Authorization Code Flow. Using the Resource Owner Password + // Credentials Grant means that the Pinniped CLI and Pinniped Supervisor will directly handle your end users' passwords + // (similar to LDAPIdentityProvider), and you will not be able to require multi-factor authentication or use the other + // web-based login features of your OIDC provider during Resource Owner Password Credentials Grant logins. + // allowPasswordGrant defaults to false. + // +optional + AllowPasswordGrant bool `json:"allowPasswordGrant,omitempty"` +} + +// Parameter is a key/value pair which represents a parameter in an HTTP request. +type Parameter struct { + // The name of the parameter. Required. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // The value of the parameter. + // +optional + Value string `json:"value,omitempty"` +} + +// OIDCClaims provides a mapping from upstream claims into identities. +type OIDCClaims struct { + // Groups provides the name of the ID token claim or userinfo endpoint response claim that will be used to ascertain + // the groups to which an identity belongs. By default, the identities will not include any group memberships when + // this setting is not configured. + // +optional + Groups string `json:"groups"` + + // Username provides the name of the ID token claim or userinfo endpoint response claim that will be used to + // ascertain an identity's username. When not set, the username will be an automatically constructed unique string + // which will include the issuer URL of your OIDC provider along with the value of the "sub" (subject) claim from + // the ID token. + // +optional + Username string `json:"username"` +} + +// OIDCClient contains information about an OIDC client (e.g., client ID and client +// secret). +type OIDCClient struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient + // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys + // "clientID" and "clientSecret". + SecretName string `json:"secretName"` +} + +// OIDCIdentityProviderSpec is the spec for configuring an OIDC identity provider. +type OIDCIdentityProviderSpec struct { + // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch + // /.well-known/openid-configuration. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://` + Issuer string `json:"issuer"` + + // TLS configuration for discovery/JWKS requests to the issuer. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` + + // AuthorizationConfig holds information about how to form the OAuth2 authorization request + // parameters to be used with this OIDC identity provider. + // +optional + AuthorizationConfig OIDCAuthorizationConfig `json:"authorizationConfig,omitempty"` + + // Claims provides the names of token claims that will be used when inspecting an identity from + // this OIDC identity provider. + // +optional + Claims OIDCClaims `json:"claims"` + + // OIDCClient contains OIDC client information to be used used with this OIDC identity + // provider. + Client OIDCClient `json:"client"` +} + +// OIDCIdentityProvider describes the configuration of an upstream OpenID Connect identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type OIDCIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec OIDCIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status OIDCIdentityProviderStatus `json:"status,omitempty"` +} + +// OIDCIdentityProviderList lists OIDCIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type OIDCIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []OIDCIdentityProvider `json:"items"` +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go new file mode 100644 index 00000000..1413a262 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go @@ -0,0 +1,11 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// Configuration for TLS parameters related to identity provider integration. +type TLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..5f5be6f3 --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,608 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProvider) DeepCopyInto(out *ActiveDirectoryIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProvider. +func (in *ActiveDirectoryIdentityProvider) DeepCopy() *ActiveDirectoryIdentityProvider { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopyInto(out *ActiveDirectoryIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderBind. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopy() *ActiveDirectoryIdentityProviderBind { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearch. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearchAttributes. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyInto(out *ActiveDirectoryIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActiveDirectoryIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderList. +func (in *ActiveDirectoryIdentityProviderList) DeepCopy() *ActiveDirectoryIdentityProviderList { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectoryIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderSpec. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopy() *ActiveDirectoryIdentityProviderSpec { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopyInto(out *ActiveDirectoryIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderStatus. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopy() *ActiveDirectoryIdentityProviderStatus { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearch. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopy() *ActiveDirectoryIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearchAttributes. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProvider. +func (in *LDAPIdentityProvider) DeepCopy() *LDAPIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderBind) DeepCopyInto(out *LDAPIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBind. +func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearch) DeepCopyInto(out *LDAPIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearch. +func (in *LDAPIdentityProviderGroupSearch) DeepCopy() *LDAPIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearchAttributes. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopy() *LDAPIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LDAPIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderList. +func (in *LDAPIdentityProviderList) DeepCopy() *LDAPIdentityProviderList { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderSpec. +func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderStatus. +func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearch) DeepCopyInto(out *LDAPIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearch. +func (in *LDAPIdentityProviderUserSearch) DeepCopy() *LDAPIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributes. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopy() *LDAPIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { + *out = *in + if in.AdditionalScopes != nil { + in, out := &in.AdditionalScopes, &out.AdditionalScopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AdditionalAuthorizeParameters != nil { + in, out := &in.AdditionalAuthorizeParameters, &out.AdditionalAuthorizeParameters + *out = make([]Parameter, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCAuthorizationConfig. +func (in *OIDCAuthorizationConfig) DeepCopy() *OIDCAuthorizationConfig { + if in == nil { + return nil + } + out := new(OIDCAuthorizationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClaims) DeepCopyInto(out *OIDCClaims) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClaims. +func (in *OIDCClaims) DeepCopy() *OIDCClaims { + if in == nil { + return nil + } + out := new(OIDCClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCClient. +func (in *OIDCClient) DeepCopy() *OIDCClient { + if in == nil { + return nil + } + out := new(OIDCClient) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCIdentityProvider) DeepCopyInto(out *OIDCIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCIdentityProvider. +func (in *OIDCIdentityProvider) DeepCopy() *OIDCIdentityProvider { + if in == nil { + return nil + } + out := new(OIDCIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OIDCIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCIdentityProviderList) DeepCopyInto(out *OIDCIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OIDCIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCIdentityProviderList. +func (in *OIDCIdentityProviderList) DeepCopy() *OIDCIdentityProviderList { + if in == nil { + return nil + } + out := new(OIDCIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OIDCIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCIdentityProviderSpec) DeepCopyInto(out *OIDCIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig) + out.Claims = in.Claims + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCIdentityProviderSpec. +func (in *OIDCIdentityProviderSpec) DeepCopy() *OIDCIdentityProviderSpec { + if in == nil { + return nil + } + out := new(OIDCIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCIdentityProviderStatus) DeepCopyInto(out *OIDCIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCIdentityProviderStatus. +func (in *OIDCIdentityProviderStatus) DeepCopy() *OIDCIdentityProviderStatus { + if in == nil { + return nil + } + out := new(OIDCIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Parameter) DeepCopyInto(out *Parameter) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Parameter. +func (in *Parameter) DeepCopy() *Parameter { + if in == nil { + return nil + } + out := new(Parameter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} diff --git a/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go new file mode 100644 index 00000000..ea055090 --- /dev/null +++ b/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -0,0 +1,66 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +// IDPType are the strings that can be returned by the Supervisor identity provider discovery endpoint +// as the "type" of each returned identity provider. +type IDPType string + +// IDPFlow are the strings that can be returned by the Supervisor identity provider discovery endpoint +// in the array of allowed client "flows" for each returned identity provider. +type IDPFlow string + +const ( + IDPTypeOIDC IDPType = "oidc" + IDPTypeLDAP IDPType = "ldap" + IDPTypeActiveDirectory IDPType = "activedirectory" + + IDPFlowCLIPassword IDPFlow = "cli_password" + IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" +) + +// Equals is a convenience function for comparing an IDPType to a string. +func (r IDPType) Equals(s string) bool { + return string(r) == s +} + +// String is a convenience function to convert an IDPType to a string. +func (r IDPType) String() string { + return string(r) +} + +// Equals is a convenience function for comparing an IDPFlow to a string. +func (r IDPFlow) Equals(s string) bool { + return string(r) == s +} + +// String is a convenience function to convert an IDPFlow to a string. +func (r IDPFlow) String() string { + return string(r) +} + +// OIDCDiscoveryResponse is part of the response from a FederationDomain's OpenID Provider Configuration +// Document returned by the .well-known/openid-configuration endpoint. It ignores all the standard OpenID Provider +// configuration metadata and only picks out the portion related to Supervisor identity provider discovery. +type OIDCDiscoveryResponse struct { + SupervisorDiscovery OIDCDiscoveryResponseIDPEndpoint `json:"discovery.supervisor.pinniped.dev/v1alpha1"` +} + +// OIDCDiscoveryResponseIDPEndpoint contains the URL for the identity provider discovery endpoint. +type OIDCDiscoveryResponseIDPEndpoint struct { + PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"` +} + +// IDPDiscoveryResponse is the response of a FederationDomain's identity provider discovery endpoint. +type IDPDiscoveryResponse struct { + PinnipedIDPs []PinnipedIDP `json:"pinniped_identity_providers"` +} + +// PinnipedIDP describes a single identity provider as included in the response of a FederationDomain's +// identity provider discovery endpoint. +type PinnipedIDP struct { + Name string `json:"name"` + Type IDPType `json:"type"` + Flows []IDPFlow `json:"flows,omitempty"` +} diff --git a/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go new file mode 100644 index 00000000..b35aafcb --- /dev/null +++ b/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go @@ -0,0 +1,25 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +// Constants related to the Supervisor FederationDomain's authorization and token endpoints. +const ( + // AuthorizeUsernameHeaderName is the name of the HTTP header which can be used to transmit a username + // to the authorize endpoint when using a password flow, for example an OIDCIdentityProvider with a password grant + // or an LDAPIdentityProvider. + AuthorizeUsernameHeaderName = "Pinniped-Username" + + // AuthorizePasswordHeaderName is the name of the HTTP header which can be used to transmit a password + // to the authorize endpoint when using a password flow, for example an OIDCIdentityProvider with a password grant + // or an LDAPIdentityProvider. + AuthorizePasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential + + // AuthorizeUpstreamIDPNameParamName is the name of the HTTP request parameter which can be used to help select which + // identity provider should be used for authentication by sending the name of the desired identity provider. + AuthorizeUpstreamIDPNameParamName = "pinniped_idp_name" + + // AuthorizeUpstreamIDPTypeParamName is the name of the HTTP request parameter which can be used to help select which + // identity provider should be used for authentication by sending the type of the desired identity provider. + AuthorizeUpstreamIDPTypeParamName = "pinniped_idp_type" +) diff --git a/generated/1.24/client/concierge/clientset/versioned/clientset.go b/generated/1.24/client/concierge/clientset/versioned/clientset.go new file mode 100644 index 00000000..c7200e9e --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/clientset.go @@ -0,0 +1,147 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + "fmt" + "net/http" + + authenticationv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1" + identityv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1" + loginv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + AuthenticationV1alpha1() authenticationv1alpha1.AuthenticationV1alpha1Interface + ConfigV1alpha1() configv1alpha1.ConfigV1alpha1Interface + IdentityV1alpha1() identityv1alpha1.IdentityV1alpha1Interface + LoginV1alpha1() loginv1alpha1.LoginV1alpha1Interface +} + +// Clientset contains the clients for groups. Each group has exactly one +// version included in a Clientset. +type Clientset struct { + *discovery.DiscoveryClient + authenticationV1alpha1 *authenticationv1alpha1.AuthenticationV1alpha1Client + configV1alpha1 *configv1alpha1.ConfigV1alpha1Client + identityV1alpha1 *identityv1alpha1.IdentityV1alpha1Client + loginV1alpha1 *loginv1alpha1.LoginV1alpha1Client +} + +// AuthenticationV1alpha1 retrieves the AuthenticationV1alpha1Client +func (c *Clientset) AuthenticationV1alpha1() authenticationv1alpha1.AuthenticationV1alpha1Interface { + return c.authenticationV1alpha1 +} + +// ConfigV1alpha1 retrieves the ConfigV1alpha1Client +func (c *Clientset) ConfigV1alpha1() configv1alpha1.ConfigV1alpha1Interface { + return c.configV1alpha1 +} + +// IdentityV1alpha1 retrieves the IdentityV1alpha1Client +func (c *Clientset) IdentityV1alpha1() identityv1alpha1.IdentityV1alpha1Interface { + return c.identityV1alpha1 +} + +// LoginV1alpha1 retrieves the LoginV1alpha1Client +func (c *Clientset) LoginV1alpha1() loginv1alpha1.LoginV1alpha1Interface { + return c.loginV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.authenticationV1alpha1, err = authenticationv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + cs.configV1alpha1, err = configv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + cs.identityV1alpha1, err = identityv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + cs.loginV1alpha1, err = loginv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.authenticationV1alpha1 = authenticationv1alpha1.New(c) + cs.configV1alpha1 = configv1alpha1.New(c) + cs.identityV1alpha1 = identityv1alpha1.New(c) + cs.loginV1alpha1 = loginv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/generated/1.24/client/concierge/clientset/versioned/doc.go b/generated/1.24/client/concierge/clientset/versioned/doc.go new file mode 100644 index 00000000..5dc02e6e --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated clientset. +package versioned diff --git a/generated/1.24/client/concierge/clientset/versioned/fake/clientset_generated.go b/generated/1.24/client/concierge/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 00000000..6676dc40 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,93 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned" + authenticationv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1" + fakeauthenticationv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake" + configv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1" + fakeconfigv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake" + identityv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1" + fakeidentityv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake" + loginv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1" + fakeloginv1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// AuthenticationV1alpha1 retrieves the AuthenticationV1alpha1Client +func (c *Clientset) AuthenticationV1alpha1() authenticationv1alpha1.AuthenticationV1alpha1Interface { + return &fakeauthenticationv1alpha1.FakeAuthenticationV1alpha1{Fake: &c.Fake} +} + +// ConfigV1alpha1 retrieves the ConfigV1alpha1Client +func (c *Clientset) ConfigV1alpha1() configv1alpha1.ConfigV1alpha1Interface { + return &fakeconfigv1alpha1.FakeConfigV1alpha1{Fake: &c.Fake} +} + +// IdentityV1alpha1 retrieves the IdentityV1alpha1Client +func (c *Clientset) IdentityV1alpha1() identityv1alpha1.IdentityV1alpha1Interface { + return &fakeidentityv1alpha1.FakeIdentityV1alpha1{Fake: &c.Fake} +} + +// LoginV1alpha1 retrieves the LoginV1alpha1Client +func (c *Clientset) LoginV1alpha1() loginv1alpha1.LoginV1alpha1Interface { + return &fakeloginv1alpha1.FakeLoginV1alpha1{Fake: &c.Fake} +} diff --git a/generated/1.24/client/concierge/clientset/versioned/fake/doc.go b/generated/1.24/client/concierge/clientset/versioned/fake/doc.go new file mode 100644 index 00000000..7c9538fd --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/generated/1.24/client/concierge/clientset/versioned/fake/register.go b/generated/1.24/client/concierge/clientset/versioned/fake/register.go new file mode 100644 index 00000000..0e829c53 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/fake/register.go @@ -0,0 +1,49 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + authenticationv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + identityv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/identity/v1alpha1" + loginv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/login/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + authenticationv1alpha1.AddToScheme, + configv1alpha1.AddToScheme, + identityv1alpha1.AddToScheme, + loginv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/generated/1.24/client/concierge/clientset/versioned/scheme/doc.go b/generated/1.24/client/concierge/clientset/versioned/scheme/doc.go new file mode 100644 index 00000000..cc02f1d3 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/scheme/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/generated/1.24/client/concierge/clientset/versioned/scheme/register.go b/generated/1.24/client/concierge/clientset/versioned/scheme/register.go new file mode 100644 index 00000000..3d14781c --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/scheme/register.go @@ -0,0 +1,49 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + authenticationv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + identityv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/identity/v1alpha1" + loginv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/login/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + authenticationv1alpha1.AddToScheme, + configv1alpha1.AddToScheme, + identityv1alpha1.AddToScheme, + loginv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/authentication_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/authentication_client.go new file mode 100644 index 00000000..23bf88ea --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/authentication_client.go @@ -0,0 +1,99 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type AuthenticationV1alpha1Interface interface { + RESTClient() rest.Interface + JWTAuthenticatorsGetter + WebhookAuthenticatorsGetter +} + +// AuthenticationV1alpha1Client is used to interact with features provided by the authentication.concierge.pinniped.dev group. +type AuthenticationV1alpha1Client struct { + restClient rest.Interface +} + +func (c *AuthenticationV1alpha1Client) JWTAuthenticators() JWTAuthenticatorInterface { + return newJWTAuthenticators(c) +} + +func (c *AuthenticationV1alpha1Client) WebhookAuthenticators() WebhookAuthenticatorInterface { + return newWebhookAuthenticators(c) +} + +// NewForConfig creates a new AuthenticationV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*AuthenticationV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new AuthenticationV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*AuthenticationV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &AuthenticationV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new AuthenticationV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *AuthenticationV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new AuthenticationV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *AuthenticationV1alpha1Client { + return &AuthenticationV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *AuthenticationV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/doc.go new file mode 100644 index 00000000..e7a470b6 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/doc.go new file mode 100644 index 00000000..7906901b --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_authentication_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_authentication_client.go new file mode 100644 index 00000000..5216e4b2 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_authentication_client.go @@ -0,0 +1,31 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeAuthenticationV1alpha1 struct { + *testing.Fake +} + +func (c *FakeAuthenticationV1alpha1) JWTAuthenticators() v1alpha1.JWTAuthenticatorInterface { + return &FakeJWTAuthenticators{c} +} + +func (c *FakeAuthenticationV1alpha1) WebhookAuthenticators() v1alpha1.WebhookAuthenticatorInterface { + return &FakeWebhookAuthenticators{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeAuthenticationV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_jwtauthenticator.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_jwtauthenticator.go new file mode 100644 index 00000000..e3c22c8c --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_jwtauthenticator.go @@ -0,0 +1,120 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeJWTAuthenticators implements JWTAuthenticatorInterface +type FakeJWTAuthenticators struct { + Fake *FakeAuthenticationV1alpha1 +} + +var jwtauthenticatorsResource = schema.GroupVersionResource{Group: "authentication.concierge.pinniped.dev", Version: "v1alpha1", Resource: "jwtauthenticators"} + +var jwtauthenticatorsKind = schema.GroupVersionKind{Group: "authentication.concierge.pinniped.dev", Version: "v1alpha1", Kind: "JWTAuthenticator"} + +// Get takes name of the jWTAuthenticator, and returns the corresponding jWTAuthenticator object, and an error if there is any. +func (c *FakeJWTAuthenticators) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.JWTAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(jwtauthenticatorsResource, name), &v1alpha1.JWTAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.JWTAuthenticator), err +} + +// List takes label and field selectors, and returns the list of JWTAuthenticators that match those selectors. +func (c *FakeJWTAuthenticators) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.JWTAuthenticatorList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(jwtauthenticatorsResource, jwtauthenticatorsKind, opts), &v1alpha1.JWTAuthenticatorList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.JWTAuthenticatorList{ListMeta: obj.(*v1alpha1.JWTAuthenticatorList).ListMeta} + for _, item := range obj.(*v1alpha1.JWTAuthenticatorList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested jWTAuthenticators. +func (c *FakeJWTAuthenticators) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(jwtauthenticatorsResource, opts)) +} + +// Create takes the representation of a jWTAuthenticator and creates it. Returns the server's representation of the jWTAuthenticator, and an error, if there is any. +func (c *FakeJWTAuthenticators) Create(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.CreateOptions) (result *v1alpha1.JWTAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(jwtauthenticatorsResource, jWTAuthenticator), &v1alpha1.JWTAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.JWTAuthenticator), err +} + +// Update takes the representation of a jWTAuthenticator and updates it. Returns the server's representation of the jWTAuthenticator, and an error, if there is any. +func (c *FakeJWTAuthenticators) Update(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.UpdateOptions) (result *v1alpha1.JWTAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(jwtauthenticatorsResource, jWTAuthenticator), &v1alpha1.JWTAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.JWTAuthenticator), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeJWTAuthenticators) UpdateStatus(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.UpdateOptions) (*v1alpha1.JWTAuthenticator, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(jwtauthenticatorsResource, "status", jWTAuthenticator), &v1alpha1.JWTAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.JWTAuthenticator), err +} + +// Delete takes name of the jWTAuthenticator and deletes it. Returns an error if one occurs. +func (c *FakeJWTAuthenticators) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(jwtauthenticatorsResource, name, opts), &v1alpha1.JWTAuthenticator{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeJWTAuthenticators) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(jwtauthenticatorsResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.JWTAuthenticatorList{}) + return err +} + +// Patch applies the patch and returns the patched jWTAuthenticator. +func (c *FakeJWTAuthenticators) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.JWTAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(jwtauthenticatorsResource, name, pt, data, subresources...), &v1alpha1.JWTAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.JWTAuthenticator), err +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_webhookauthenticator.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_webhookauthenticator.go new file mode 100644 index 00000000..2f78b634 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/fake/fake_webhookauthenticator.go @@ -0,0 +1,120 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeWebhookAuthenticators implements WebhookAuthenticatorInterface +type FakeWebhookAuthenticators struct { + Fake *FakeAuthenticationV1alpha1 +} + +var webhookauthenticatorsResource = schema.GroupVersionResource{Group: "authentication.concierge.pinniped.dev", Version: "v1alpha1", Resource: "webhookauthenticators"} + +var webhookauthenticatorsKind = schema.GroupVersionKind{Group: "authentication.concierge.pinniped.dev", Version: "v1alpha1", Kind: "WebhookAuthenticator"} + +// Get takes name of the webhookAuthenticator, and returns the corresponding webhookAuthenticator object, and an error if there is any. +func (c *FakeWebhookAuthenticators) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(webhookauthenticatorsResource, name), &v1alpha1.WebhookAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.WebhookAuthenticator), err +} + +// List takes label and field selectors, and returns the list of WebhookAuthenticators that match those selectors. +func (c *FakeWebhookAuthenticators) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.WebhookAuthenticatorList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(webhookauthenticatorsResource, webhookauthenticatorsKind, opts), &v1alpha1.WebhookAuthenticatorList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.WebhookAuthenticatorList{ListMeta: obj.(*v1alpha1.WebhookAuthenticatorList).ListMeta} + for _, item := range obj.(*v1alpha1.WebhookAuthenticatorList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested webhookAuthenticators. +func (c *FakeWebhookAuthenticators) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(webhookauthenticatorsResource, opts)) +} + +// Create takes the representation of a webhookAuthenticator and creates it. Returns the server's representation of the webhookAuthenticator, and an error, if there is any. +func (c *FakeWebhookAuthenticators) Create(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.CreateOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(webhookauthenticatorsResource, webhookAuthenticator), &v1alpha1.WebhookAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.WebhookAuthenticator), err +} + +// Update takes the representation of a webhookAuthenticator and updates it. Returns the server's representation of the webhookAuthenticator, and an error, if there is any. +func (c *FakeWebhookAuthenticators) Update(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.UpdateOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(webhookauthenticatorsResource, webhookAuthenticator), &v1alpha1.WebhookAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.WebhookAuthenticator), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeWebhookAuthenticators) UpdateStatus(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.UpdateOptions) (*v1alpha1.WebhookAuthenticator, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(webhookauthenticatorsResource, "status", webhookAuthenticator), &v1alpha1.WebhookAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.WebhookAuthenticator), err +} + +// Delete takes name of the webhookAuthenticator and deletes it. Returns an error if one occurs. +func (c *FakeWebhookAuthenticators) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(webhookauthenticatorsResource, name, opts), &v1alpha1.WebhookAuthenticator{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeWebhookAuthenticators) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(webhookauthenticatorsResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.WebhookAuthenticatorList{}) + return err +} + +// Patch applies the patch and returns the patched webhookAuthenticator. +func (c *FakeWebhookAuthenticators) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.WebhookAuthenticator, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(webhookauthenticatorsResource, name, pt, data, subresources...), &v1alpha1.WebhookAuthenticator{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.WebhookAuthenticator), err +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/generated_expansion.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..03757746 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/generated_expansion.go @@ -0,0 +1,10 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type JWTAuthenticatorExpansion interface{} + +type WebhookAuthenticatorExpansion interface{} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/jwtauthenticator.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/jwtauthenticator.go new file mode 100644 index 00000000..d66cca5a --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/jwtauthenticator.go @@ -0,0 +1,171 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// JWTAuthenticatorsGetter has a method to return a JWTAuthenticatorInterface. +// A group's client should implement this interface. +type JWTAuthenticatorsGetter interface { + JWTAuthenticators() JWTAuthenticatorInterface +} + +// JWTAuthenticatorInterface has methods to work with JWTAuthenticator resources. +type JWTAuthenticatorInterface interface { + Create(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.CreateOptions) (*v1alpha1.JWTAuthenticator, error) + Update(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.UpdateOptions) (*v1alpha1.JWTAuthenticator, error) + UpdateStatus(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.UpdateOptions) (*v1alpha1.JWTAuthenticator, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.JWTAuthenticator, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.JWTAuthenticatorList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.JWTAuthenticator, err error) + JWTAuthenticatorExpansion +} + +// jWTAuthenticators implements JWTAuthenticatorInterface +type jWTAuthenticators struct { + client rest.Interface +} + +// newJWTAuthenticators returns a JWTAuthenticators +func newJWTAuthenticators(c *AuthenticationV1alpha1Client) *jWTAuthenticators { + return &jWTAuthenticators{ + client: c.RESTClient(), + } +} + +// Get takes name of the jWTAuthenticator, and returns the corresponding jWTAuthenticator object, and an error if there is any. +func (c *jWTAuthenticators) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.JWTAuthenticator, err error) { + result = &v1alpha1.JWTAuthenticator{} + err = c.client.Get(). + Resource("jwtauthenticators"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of JWTAuthenticators that match those selectors. +func (c *jWTAuthenticators) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.JWTAuthenticatorList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.JWTAuthenticatorList{} + err = c.client.Get(). + Resource("jwtauthenticators"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested jWTAuthenticators. +func (c *jWTAuthenticators) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("jwtauthenticators"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a jWTAuthenticator and creates it. Returns the server's representation of the jWTAuthenticator, and an error, if there is any. +func (c *jWTAuthenticators) Create(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.CreateOptions) (result *v1alpha1.JWTAuthenticator, err error) { + result = &v1alpha1.JWTAuthenticator{} + err = c.client.Post(). + Resource("jwtauthenticators"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(jWTAuthenticator). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a jWTAuthenticator and updates it. Returns the server's representation of the jWTAuthenticator, and an error, if there is any. +func (c *jWTAuthenticators) Update(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.UpdateOptions) (result *v1alpha1.JWTAuthenticator, err error) { + result = &v1alpha1.JWTAuthenticator{} + err = c.client.Put(). + Resource("jwtauthenticators"). + Name(jWTAuthenticator.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(jWTAuthenticator). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *jWTAuthenticators) UpdateStatus(ctx context.Context, jWTAuthenticator *v1alpha1.JWTAuthenticator, opts v1.UpdateOptions) (result *v1alpha1.JWTAuthenticator, err error) { + result = &v1alpha1.JWTAuthenticator{} + err = c.client.Put(). + Resource("jwtauthenticators"). + Name(jWTAuthenticator.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(jWTAuthenticator). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the jWTAuthenticator and deletes it. Returns an error if one occurs. +func (c *jWTAuthenticators) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("jwtauthenticators"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *jWTAuthenticators) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("jwtauthenticators"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched jWTAuthenticator. +func (c *jWTAuthenticators) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.JWTAuthenticator, err error) { + result = &v1alpha1.JWTAuthenticator{} + err = c.client.Patch(pt). + Resource("jwtauthenticators"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/webhookauthenticator.go b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/webhookauthenticator.go new file mode 100644 index 00000000..237d5874 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/authentication/v1alpha1/webhookauthenticator.go @@ -0,0 +1,171 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// WebhookAuthenticatorsGetter has a method to return a WebhookAuthenticatorInterface. +// A group's client should implement this interface. +type WebhookAuthenticatorsGetter interface { + WebhookAuthenticators() WebhookAuthenticatorInterface +} + +// WebhookAuthenticatorInterface has methods to work with WebhookAuthenticator resources. +type WebhookAuthenticatorInterface interface { + Create(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.CreateOptions) (*v1alpha1.WebhookAuthenticator, error) + Update(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.UpdateOptions) (*v1alpha1.WebhookAuthenticator, error) + UpdateStatus(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.UpdateOptions) (*v1alpha1.WebhookAuthenticator, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.WebhookAuthenticator, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.WebhookAuthenticatorList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.WebhookAuthenticator, err error) + WebhookAuthenticatorExpansion +} + +// webhookAuthenticators implements WebhookAuthenticatorInterface +type webhookAuthenticators struct { + client rest.Interface +} + +// newWebhookAuthenticators returns a WebhookAuthenticators +func newWebhookAuthenticators(c *AuthenticationV1alpha1Client) *webhookAuthenticators { + return &webhookAuthenticators{ + client: c.RESTClient(), + } +} + +// Get takes name of the webhookAuthenticator, and returns the corresponding webhookAuthenticator object, and an error if there is any. +func (c *webhookAuthenticators) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + result = &v1alpha1.WebhookAuthenticator{} + err = c.client.Get(). + Resource("webhookauthenticators"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of WebhookAuthenticators that match those selectors. +func (c *webhookAuthenticators) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.WebhookAuthenticatorList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.WebhookAuthenticatorList{} + err = c.client.Get(). + Resource("webhookauthenticators"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested webhookAuthenticators. +func (c *webhookAuthenticators) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("webhookauthenticators"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a webhookAuthenticator and creates it. Returns the server's representation of the webhookAuthenticator, and an error, if there is any. +func (c *webhookAuthenticators) Create(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.CreateOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + result = &v1alpha1.WebhookAuthenticator{} + err = c.client.Post(). + Resource("webhookauthenticators"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(webhookAuthenticator). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a webhookAuthenticator and updates it. Returns the server's representation of the webhookAuthenticator, and an error, if there is any. +func (c *webhookAuthenticators) Update(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.UpdateOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + result = &v1alpha1.WebhookAuthenticator{} + err = c.client.Put(). + Resource("webhookauthenticators"). + Name(webhookAuthenticator.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(webhookAuthenticator). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *webhookAuthenticators) UpdateStatus(ctx context.Context, webhookAuthenticator *v1alpha1.WebhookAuthenticator, opts v1.UpdateOptions) (result *v1alpha1.WebhookAuthenticator, err error) { + result = &v1alpha1.WebhookAuthenticator{} + err = c.client.Put(). + Resource("webhookauthenticators"). + Name(webhookAuthenticator.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(webhookAuthenticator). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the webhookAuthenticator and deletes it. Returns an error if one occurs. +func (c *webhookAuthenticators) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("webhookauthenticators"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *webhookAuthenticators) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("webhookauthenticators"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched webhookAuthenticator. +func (c *webhookAuthenticators) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.WebhookAuthenticator, err error) { + result = &v1alpha1.WebhookAuthenticator{} + err = c.client.Patch(pt). + Resource("webhookauthenticators"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/config_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/config_client.go new file mode 100644 index 00000000..62500499 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/config_client.go @@ -0,0 +1,94 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type ConfigV1alpha1Interface interface { + RESTClient() rest.Interface + CredentialIssuersGetter +} + +// ConfigV1alpha1Client is used to interact with features provided by the config.concierge.pinniped.dev group. +type ConfigV1alpha1Client struct { + restClient rest.Interface +} + +func (c *ConfigV1alpha1Client) CredentialIssuers() CredentialIssuerInterface { + return newCredentialIssuers(c) +} + +// NewForConfig creates a new ConfigV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*ConfigV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new ConfigV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ConfigV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &ConfigV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new ConfigV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *ConfigV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new ConfigV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *ConfigV1alpha1Client { + return &ConfigV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *ConfigV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/credentialissuer.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/credentialissuer.go new file mode 100644 index 00000000..011e38be --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/credentialissuer.go @@ -0,0 +1,171 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// CredentialIssuersGetter has a method to return a CredentialIssuerInterface. +// A group's client should implement this interface. +type CredentialIssuersGetter interface { + CredentialIssuers() CredentialIssuerInterface +} + +// CredentialIssuerInterface has methods to work with CredentialIssuer resources. +type CredentialIssuerInterface interface { + Create(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.CreateOptions) (*v1alpha1.CredentialIssuer, error) + Update(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.UpdateOptions) (*v1alpha1.CredentialIssuer, error) + UpdateStatus(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.UpdateOptions) (*v1alpha1.CredentialIssuer, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.CredentialIssuer, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.CredentialIssuerList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CredentialIssuer, err error) + CredentialIssuerExpansion +} + +// credentialIssuers implements CredentialIssuerInterface +type credentialIssuers struct { + client rest.Interface +} + +// newCredentialIssuers returns a CredentialIssuers +func newCredentialIssuers(c *ConfigV1alpha1Client) *credentialIssuers { + return &credentialIssuers{ + client: c.RESTClient(), + } +} + +// Get takes name of the credentialIssuer, and returns the corresponding credentialIssuer object, and an error if there is any. +func (c *credentialIssuers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CredentialIssuer, err error) { + result = &v1alpha1.CredentialIssuer{} + err = c.client.Get(). + Resource("credentialissuers"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of CredentialIssuers that match those selectors. +func (c *credentialIssuers) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CredentialIssuerList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.CredentialIssuerList{} + err = c.client.Get(). + Resource("credentialissuers"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested credentialIssuers. +func (c *credentialIssuers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("credentialissuers"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a credentialIssuer and creates it. Returns the server's representation of the credentialIssuer, and an error, if there is any. +func (c *credentialIssuers) Create(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.CreateOptions) (result *v1alpha1.CredentialIssuer, err error) { + result = &v1alpha1.CredentialIssuer{} + err = c.client.Post(). + Resource("credentialissuers"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(credentialIssuer). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a credentialIssuer and updates it. Returns the server's representation of the credentialIssuer, and an error, if there is any. +func (c *credentialIssuers) Update(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.UpdateOptions) (result *v1alpha1.CredentialIssuer, err error) { + result = &v1alpha1.CredentialIssuer{} + err = c.client.Put(). + Resource("credentialissuers"). + Name(credentialIssuer.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(credentialIssuer). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *credentialIssuers) UpdateStatus(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.UpdateOptions) (result *v1alpha1.CredentialIssuer, err error) { + result = &v1alpha1.CredentialIssuer{} + err = c.client.Put(). + Resource("credentialissuers"). + Name(credentialIssuer.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(credentialIssuer). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the credentialIssuer and deletes it. Returns an error if one occurs. +func (c *credentialIssuers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("credentialissuers"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *credentialIssuers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("credentialissuers"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched credentialIssuer. +func (c *credentialIssuers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CredentialIssuer, err error) { + result = &v1alpha1.CredentialIssuer{} + err = c.client.Patch(pt). + Resource("credentialissuers"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/doc.go new file mode 100644 index 00000000..e7a470b6 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/doc.go new file mode 100644 index 00000000..7906901b --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go new file mode 100644 index 00000000..ef8fb4fe --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go @@ -0,0 +1,27 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeConfigV1alpha1 struct { + *testing.Fake +} + +func (c *FakeConfigV1alpha1) CredentialIssuers() v1alpha1.CredentialIssuerInterface { + return &FakeCredentialIssuers{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeConfigV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_credentialissuer.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_credentialissuer.go new file mode 100644 index 00000000..a9969ce2 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/fake/fake_credentialissuer.go @@ -0,0 +1,120 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeCredentialIssuers implements CredentialIssuerInterface +type FakeCredentialIssuers struct { + Fake *FakeConfigV1alpha1 +} + +var credentialissuersResource = schema.GroupVersionResource{Group: "config.concierge.pinniped.dev", Version: "v1alpha1", Resource: "credentialissuers"} + +var credentialissuersKind = schema.GroupVersionKind{Group: "config.concierge.pinniped.dev", Version: "v1alpha1", Kind: "CredentialIssuer"} + +// Get takes name of the credentialIssuer, and returns the corresponding credentialIssuer object, and an error if there is any. +func (c *FakeCredentialIssuers) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.CredentialIssuer, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(credentialissuersResource, name), &v1alpha1.CredentialIssuer{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CredentialIssuer), err +} + +// List takes label and field selectors, and returns the list of CredentialIssuers that match those selectors. +func (c *FakeCredentialIssuers) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.CredentialIssuerList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(credentialissuersResource, credentialissuersKind, opts), &v1alpha1.CredentialIssuerList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.CredentialIssuerList{ListMeta: obj.(*v1alpha1.CredentialIssuerList).ListMeta} + for _, item := range obj.(*v1alpha1.CredentialIssuerList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested credentialIssuers. +func (c *FakeCredentialIssuers) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(credentialissuersResource, opts)) +} + +// Create takes the representation of a credentialIssuer and creates it. Returns the server's representation of the credentialIssuer, and an error, if there is any. +func (c *FakeCredentialIssuers) Create(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.CreateOptions) (result *v1alpha1.CredentialIssuer, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(credentialissuersResource, credentialIssuer), &v1alpha1.CredentialIssuer{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CredentialIssuer), err +} + +// Update takes the representation of a credentialIssuer and updates it. Returns the server's representation of the credentialIssuer, and an error, if there is any. +func (c *FakeCredentialIssuers) Update(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.UpdateOptions) (result *v1alpha1.CredentialIssuer, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(credentialissuersResource, credentialIssuer), &v1alpha1.CredentialIssuer{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CredentialIssuer), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeCredentialIssuers) UpdateStatus(ctx context.Context, credentialIssuer *v1alpha1.CredentialIssuer, opts v1.UpdateOptions) (*v1alpha1.CredentialIssuer, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(credentialissuersResource, "status", credentialIssuer), &v1alpha1.CredentialIssuer{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CredentialIssuer), err +} + +// Delete takes name of the credentialIssuer and deletes it. Returns an error if one occurs. +func (c *FakeCredentialIssuers) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(credentialissuersResource, name, opts), &v1alpha1.CredentialIssuer{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeCredentialIssuers) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(credentialissuersResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.CredentialIssuerList{}) + return err +} + +// Patch applies the patch and returns the patched credentialIssuer. +func (c *FakeCredentialIssuers) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.CredentialIssuer, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(credentialissuersResource, name, pt, data, subresources...), &v1alpha1.CredentialIssuer{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.CredentialIssuer), err +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/generated_expansion.go b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..294cd402 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/config/v1alpha1/generated_expansion.go @@ -0,0 +1,8 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type CredentialIssuerExpansion interface{} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/doc.go new file mode 100644 index 00000000..e7a470b6 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/doc.go new file mode 100644 index 00000000..7906901b --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_identity_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_identity_client.go new file mode 100644 index 00000000..f6d0df68 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_identity_client.go @@ -0,0 +1,27 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeIdentityV1alpha1 struct { + *testing.Fake +} + +func (c *FakeIdentityV1alpha1) WhoAmIRequests() v1alpha1.WhoAmIRequestInterface { + return &FakeWhoAmIRequests{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeIdentityV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_whoamirequest.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_whoamirequest.go new file mode 100644 index 00000000..77cd5c6d --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/fake/fake_whoamirequest.go @@ -0,0 +1,34 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/identity/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + testing "k8s.io/client-go/testing" +) + +// FakeWhoAmIRequests implements WhoAmIRequestInterface +type FakeWhoAmIRequests struct { + Fake *FakeIdentityV1alpha1 +} + +var whoamirequestsResource = schema.GroupVersionResource{Group: "identity.concierge.pinniped.dev", Version: "v1alpha1", Resource: "whoamirequests"} + +var whoamirequestsKind = schema.GroupVersionKind{Group: "identity.concierge.pinniped.dev", Version: "v1alpha1", Kind: "WhoAmIRequest"} + +// Create takes the representation of a whoAmIRequest and creates it. Returns the server's representation of the whoAmIRequest, and an error, if there is any. +func (c *FakeWhoAmIRequests) Create(ctx context.Context, whoAmIRequest *v1alpha1.WhoAmIRequest, opts v1.CreateOptions) (result *v1alpha1.WhoAmIRequest, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(whoamirequestsResource, whoAmIRequest), &v1alpha1.WhoAmIRequest{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.WhoAmIRequest), err +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/generated_expansion.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..925d9ca3 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/generated_expansion.go @@ -0,0 +1,8 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type WhoAmIRequestExpansion interface{} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/identity_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/identity_client.go new file mode 100644 index 00000000..65d486f9 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/identity_client.go @@ -0,0 +1,94 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/identity/v1alpha1" + "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type IdentityV1alpha1Interface interface { + RESTClient() rest.Interface + WhoAmIRequestsGetter +} + +// IdentityV1alpha1Client is used to interact with features provided by the identity.concierge.pinniped.dev group. +type IdentityV1alpha1Client struct { + restClient rest.Interface +} + +func (c *IdentityV1alpha1Client) WhoAmIRequests() WhoAmIRequestInterface { + return newWhoAmIRequests(c) +} + +// NewForConfig creates a new IdentityV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*IdentityV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new IdentityV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*IdentityV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &IdentityV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new IdentityV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *IdentityV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new IdentityV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *IdentityV1alpha1Client { + return &IdentityV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *IdentityV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/whoamirequest.go b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/whoamirequest.go new file mode 100644 index 00000000..07fd298e --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/identity/v1alpha1/whoamirequest.go @@ -0,0 +1,51 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/identity/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rest "k8s.io/client-go/rest" +) + +// WhoAmIRequestsGetter has a method to return a WhoAmIRequestInterface. +// A group's client should implement this interface. +type WhoAmIRequestsGetter interface { + WhoAmIRequests() WhoAmIRequestInterface +} + +// WhoAmIRequestInterface has methods to work with WhoAmIRequest resources. +type WhoAmIRequestInterface interface { + Create(ctx context.Context, whoAmIRequest *v1alpha1.WhoAmIRequest, opts v1.CreateOptions) (*v1alpha1.WhoAmIRequest, error) + WhoAmIRequestExpansion +} + +// whoAmIRequests implements WhoAmIRequestInterface +type whoAmIRequests struct { + client rest.Interface +} + +// newWhoAmIRequests returns a WhoAmIRequests +func newWhoAmIRequests(c *IdentityV1alpha1Client) *whoAmIRequests { + return &whoAmIRequests{ + client: c.RESTClient(), + } +} + +// Create takes the representation of a whoAmIRequest and creates it. Returns the server's representation of the whoAmIRequest, and an error, if there is any. +func (c *whoAmIRequests) Create(ctx context.Context, whoAmIRequest *v1alpha1.WhoAmIRequest, opts v1.CreateOptions) (result *v1alpha1.WhoAmIRequest, err error) { + result = &v1alpha1.WhoAmIRequest{} + err = c.client.Post(). + Resource("whoamirequests"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(whoAmIRequest). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/doc.go new file mode 100644 index 00000000..e7a470b6 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/doc.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/doc.go new file mode 100644 index 00000000..7906901b --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_login_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_login_client.go new file mode 100644 index 00000000..0f942f29 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_login_client.go @@ -0,0 +1,27 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeLoginV1alpha1 struct { + *testing.Fake +} + +func (c *FakeLoginV1alpha1) TokenCredentialRequests() v1alpha1.TokenCredentialRequestInterface { + return &FakeTokenCredentialRequests{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeLoginV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_tokencredentialrequest.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_tokencredentialrequest.go new file mode 100644 index 00000000..03fa4625 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/fake/fake_tokencredentialrequest.go @@ -0,0 +1,34 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/login/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + testing "k8s.io/client-go/testing" +) + +// FakeTokenCredentialRequests implements TokenCredentialRequestInterface +type FakeTokenCredentialRequests struct { + Fake *FakeLoginV1alpha1 +} + +var tokencredentialrequestsResource = schema.GroupVersionResource{Group: "login.concierge.pinniped.dev", Version: "v1alpha1", Resource: "tokencredentialrequests"} + +var tokencredentialrequestsKind = schema.GroupVersionKind{Group: "login.concierge.pinniped.dev", Version: "v1alpha1", Kind: "TokenCredentialRequest"} + +// Create takes the representation of a tokenCredentialRequest and creates it. Returns the server's representation of the tokenCredentialRequest, and an error, if there is any. +func (c *FakeTokenCredentialRequests) Create(ctx context.Context, tokenCredentialRequest *v1alpha1.TokenCredentialRequest, opts v1.CreateOptions) (result *v1alpha1.TokenCredentialRequest, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(tokencredentialrequestsResource, tokenCredentialRequest), &v1alpha1.TokenCredentialRequest{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.TokenCredentialRequest), err +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/generated_expansion.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..7ed35ca8 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/generated_expansion.go @@ -0,0 +1,8 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type TokenCredentialRequestExpansion interface{} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/login_client.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/login_client.go new file mode 100644 index 00000000..0e0b3a49 --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/login_client.go @@ -0,0 +1,94 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/login/v1alpha1" + "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type LoginV1alpha1Interface interface { + RESTClient() rest.Interface + TokenCredentialRequestsGetter +} + +// LoginV1alpha1Client is used to interact with features provided by the login.concierge.pinniped.dev group. +type LoginV1alpha1Client struct { + restClient rest.Interface +} + +func (c *LoginV1alpha1Client) TokenCredentialRequests() TokenCredentialRequestInterface { + return newTokenCredentialRequests(c) +} + +// NewForConfig creates a new LoginV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*LoginV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new LoginV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*LoginV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &LoginV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new LoginV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *LoginV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new LoginV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *LoginV1alpha1Client { + return &LoginV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *LoginV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/tokencredentialrequest.go b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/tokencredentialrequest.go new file mode 100644 index 00000000..8767633d --- /dev/null +++ b/generated/1.24/client/concierge/clientset/versioned/typed/login/v1alpha1/tokencredentialrequest.go @@ -0,0 +1,51 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/login/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rest "k8s.io/client-go/rest" +) + +// TokenCredentialRequestsGetter has a method to return a TokenCredentialRequestInterface. +// A group's client should implement this interface. +type TokenCredentialRequestsGetter interface { + TokenCredentialRequests() TokenCredentialRequestInterface +} + +// TokenCredentialRequestInterface has methods to work with TokenCredentialRequest resources. +type TokenCredentialRequestInterface interface { + Create(ctx context.Context, tokenCredentialRequest *v1alpha1.TokenCredentialRequest, opts v1.CreateOptions) (*v1alpha1.TokenCredentialRequest, error) + TokenCredentialRequestExpansion +} + +// tokenCredentialRequests implements TokenCredentialRequestInterface +type tokenCredentialRequests struct { + client rest.Interface +} + +// newTokenCredentialRequests returns a TokenCredentialRequests +func newTokenCredentialRequests(c *LoginV1alpha1Client) *tokenCredentialRequests { + return &tokenCredentialRequests{ + client: c.RESTClient(), + } +} + +// Create takes the representation of a tokenCredentialRequest and creates it. Returns the server's representation of the tokenCredentialRequest, and an error, if there is any. +func (c *tokenCredentialRequests) Create(ctx context.Context, tokenCredentialRequest *v1alpha1.TokenCredentialRequest, opts v1.CreateOptions) (result *v1alpha1.TokenCredentialRequest, err error) { + result = &v1alpha1.TokenCredentialRequest{} + err = c.client.Post(). + Resource("tokencredentialrequests"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(tokenCredentialRequest). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/concierge/informers/externalversions/authentication/interface.go b/generated/1.24/client/concierge/informers/externalversions/authentication/interface.go new file mode 100644 index 00000000..26730193 --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/authentication/interface.go @@ -0,0 +1,33 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package authentication + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1" + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/interface.go b/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/interface.go new file mode 100644 index 00000000..c910b13c --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/interface.go @@ -0,0 +1,39 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // JWTAuthenticators returns a JWTAuthenticatorInformer. + JWTAuthenticators() JWTAuthenticatorInformer + // WebhookAuthenticators returns a WebhookAuthenticatorInformer. + WebhookAuthenticators() WebhookAuthenticatorInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// JWTAuthenticators returns a JWTAuthenticatorInformer. +func (v *version) JWTAuthenticators() JWTAuthenticatorInformer { + return &jWTAuthenticatorInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + +// WebhookAuthenticators returns a WebhookAuthenticatorInformer. +func (v *version) WebhookAuthenticators() WebhookAuthenticatorInformer { + return &webhookAuthenticatorInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/jwtauthenticator.go b/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/jwtauthenticator.go new file mode 100644 index 00000000..7093450e --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/jwtauthenticator.go @@ -0,0 +1,76 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + authenticationv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/listers/authentication/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// JWTAuthenticatorInformer provides access to a shared informer and lister for +// JWTAuthenticators. +type JWTAuthenticatorInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.JWTAuthenticatorLister +} + +type jWTAuthenticatorInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewJWTAuthenticatorInformer constructs a new informer for JWTAuthenticator type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewJWTAuthenticatorInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredJWTAuthenticatorInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredJWTAuthenticatorInformer constructs a new informer for JWTAuthenticator type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredJWTAuthenticatorInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AuthenticationV1alpha1().JWTAuthenticators().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AuthenticationV1alpha1().JWTAuthenticators().Watch(context.TODO(), options) + }, + }, + &authenticationv1alpha1.JWTAuthenticator{}, + resyncPeriod, + indexers, + ) +} + +func (f *jWTAuthenticatorInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredJWTAuthenticatorInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *jWTAuthenticatorInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&authenticationv1alpha1.JWTAuthenticator{}, f.defaultInformer) +} + +func (f *jWTAuthenticatorInformer) Lister() v1alpha1.JWTAuthenticatorLister { + return v1alpha1.NewJWTAuthenticatorLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/webhookauthenticator.go b/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/webhookauthenticator.go new file mode 100644 index 00000000..b3c76b84 --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/authentication/v1alpha1/webhookauthenticator.go @@ -0,0 +1,76 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + authenticationv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/listers/authentication/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// WebhookAuthenticatorInformer provides access to a shared informer and lister for +// WebhookAuthenticators. +type WebhookAuthenticatorInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.WebhookAuthenticatorLister +} + +type webhookAuthenticatorInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewWebhookAuthenticatorInformer constructs a new informer for WebhookAuthenticator type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewWebhookAuthenticatorInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredWebhookAuthenticatorInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredWebhookAuthenticatorInformer constructs a new informer for WebhookAuthenticator type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredWebhookAuthenticatorInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AuthenticationV1alpha1().WebhookAuthenticators().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.AuthenticationV1alpha1().WebhookAuthenticators().Watch(context.TODO(), options) + }, + }, + &authenticationv1alpha1.WebhookAuthenticator{}, + resyncPeriod, + indexers, + ) +} + +func (f *webhookAuthenticatorInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredWebhookAuthenticatorInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *webhookAuthenticatorInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&authenticationv1alpha1.WebhookAuthenticator{}, f.defaultInformer) +} + +func (f *webhookAuthenticatorInformer) Lister() v1alpha1.WebhookAuthenticatorLister { + return v1alpha1.NewWebhookAuthenticatorLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/config/interface.go b/generated/1.24/client/concierge/informers/externalversions/config/interface.go new file mode 100644 index 00000000..d7bfc33c --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/config/interface.go @@ -0,0 +1,33 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package config + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1" + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/credentialissuer.go b/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/credentialissuer.go new file mode 100644 index 00000000..218f1c59 --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/credentialissuer.go @@ -0,0 +1,76 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/concierge/listers/config/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CredentialIssuerInformer provides access to a shared informer and lister for +// CredentialIssuers. +type CredentialIssuerInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.CredentialIssuerLister +} + +type credentialIssuerInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewCredentialIssuerInformer constructs a new informer for CredentialIssuer type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCredentialIssuerInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCredentialIssuerInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredCredentialIssuerInformer constructs a new informer for CredentialIssuer type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCredentialIssuerInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ConfigV1alpha1().CredentialIssuers().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ConfigV1alpha1().CredentialIssuers().Watch(context.TODO(), options) + }, + }, + &configv1alpha1.CredentialIssuer{}, + resyncPeriod, + indexers, + ) +} + +func (f *credentialIssuerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCredentialIssuerInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *credentialIssuerInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&configv1alpha1.CredentialIssuer{}, f.defaultInformer) +} + +func (f *credentialIssuerInformer) Lister() v1alpha1.CredentialIssuerLister { + return v1alpha1.NewCredentialIssuerLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/interface.go b/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/interface.go new file mode 100644 index 00000000..f8c74e80 --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/config/v1alpha1/interface.go @@ -0,0 +1,32 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // CredentialIssuers returns a CredentialIssuerInformer. + CredentialIssuers() CredentialIssuerInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// CredentialIssuers returns a CredentialIssuerInformer. +func (v *version) CredentialIssuers() CredentialIssuerInformer { + return &credentialIssuerInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/generated/1.24/client/concierge/informers/externalversions/factory.go b/generated/1.24/client/concierge/informers/externalversions/factory.go new file mode 100644 index 00000000..7a9e8f07 --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/factory.go @@ -0,0 +1,173 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned" + authentication "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/authentication" + config "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/config" + internalinterfaces "go.pinniped.dev/generated/1.24/client/concierge/informers/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +// Start initializes all requested informers. +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InternalInformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + Authentication() authentication.Interface + Config() config.Interface +} + +func (f *sharedInformerFactory) Authentication() authentication.Interface { + return authentication.New(f, f.namespace, f.tweakListOptions) +} + +func (f *sharedInformerFactory) Config() config.Interface { + return config.New(f, f.namespace, f.tweakListOptions) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/generic.go b/generated/1.24/client/concierge/informers/externalversions/generic.go new file mode 100644 index 00000000..c7a6984a --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/generic.go @@ -0,0 +1,56 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + "fmt" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=authentication.concierge.pinniped.dev, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("jwtauthenticators"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Authentication().V1alpha1().JWTAuthenticators().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("webhookauthenticators"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Authentication().V1alpha1().WebhookAuthenticators().Informer()}, nil + + // Group=config.concierge.pinniped.dev, Version=v1alpha1 + case configv1alpha1.SchemeGroupVersion.WithResource("credentialissuers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().CredentialIssuers().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/generated/1.24/client/concierge/informers/externalversions/internalinterfaces/factory_interfaces.go b/generated/1.24/client/concierge/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 00000000..f976b2e9 --- /dev/null +++ b/generated/1.24/client/concierge/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,27 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "go.pinniped.dev/generated/1.24/client/concierge/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/generated/1.24/client/concierge/listers/authentication/v1alpha1/expansion_generated.go b/generated/1.24/client/concierge/listers/authentication/v1alpha1/expansion_generated.go new file mode 100644 index 00000000..665b978e --- /dev/null +++ b/generated/1.24/client/concierge/listers/authentication/v1alpha1/expansion_generated.go @@ -0,0 +1,14 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// JWTAuthenticatorListerExpansion allows custom methods to be added to +// JWTAuthenticatorLister. +type JWTAuthenticatorListerExpansion interface{} + +// WebhookAuthenticatorListerExpansion allows custom methods to be added to +// WebhookAuthenticatorLister. +type WebhookAuthenticatorListerExpansion interface{} diff --git a/generated/1.24/client/concierge/listers/authentication/v1alpha1/jwtauthenticator.go b/generated/1.24/client/concierge/listers/authentication/v1alpha1/jwtauthenticator.go new file mode 100644 index 00000000..2b2ae08e --- /dev/null +++ b/generated/1.24/client/concierge/listers/authentication/v1alpha1/jwtauthenticator.go @@ -0,0 +1,55 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// JWTAuthenticatorLister helps list JWTAuthenticators. +// All objects returned here must be treated as read-only. +type JWTAuthenticatorLister interface { + // List lists all JWTAuthenticators in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.JWTAuthenticator, err error) + // Get retrieves the JWTAuthenticator from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.JWTAuthenticator, error) + JWTAuthenticatorListerExpansion +} + +// jWTAuthenticatorLister implements the JWTAuthenticatorLister interface. +type jWTAuthenticatorLister struct { + indexer cache.Indexer +} + +// NewJWTAuthenticatorLister returns a new JWTAuthenticatorLister. +func NewJWTAuthenticatorLister(indexer cache.Indexer) JWTAuthenticatorLister { + return &jWTAuthenticatorLister{indexer: indexer} +} + +// List lists all JWTAuthenticators in the indexer. +func (s *jWTAuthenticatorLister) List(selector labels.Selector) (ret []*v1alpha1.JWTAuthenticator, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.JWTAuthenticator)) + }) + return ret, err +} + +// Get retrieves the JWTAuthenticator from the index for a given name. +func (s *jWTAuthenticatorLister) Get(name string) (*v1alpha1.JWTAuthenticator, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("jwtauthenticator"), name) + } + return obj.(*v1alpha1.JWTAuthenticator), nil +} diff --git a/generated/1.24/client/concierge/listers/authentication/v1alpha1/webhookauthenticator.go b/generated/1.24/client/concierge/listers/authentication/v1alpha1/webhookauthenticator.go new file mode 100644 index 00000000..8ebf09ab --- /dev/null +++ b/generated/1.24/client/concierge/listers/authentication/v1alpha1/webhookauthenticator.go @@ -0,0 +1,55 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/authentication/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// WebhookAuthenticatorLister helps list WebhookAuthenticators. +// All objects returned here must be treated as read-only. +type WebhookAuthenticatorLister interface { + // List lists all WebhookAuthenticators in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.WebhookAuthenticator, err error) + // Get retrieves the WebhookAuthenticator from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.WebhookAuthenticator, error) + WebhookAuthenticatorListerExpansion +} + +// webhookAuthenticatorLister implements the WebhookAuthenticatorLister interface. +type webhookAuthenticatorLister struct { + indexer cache.Indexer +} + +// NewWebhookAuthenticatorLister returns a new WebhookAuthenticatorLister. +func NewWebhookAuthenticatorLister(indexer cache.Indexer) WebhookAuthenticatorLister { + return &webhookAuthenticatorLister{indexer: indexer} +} + +// List lists all WebhookAuthenticators in the indexer. +func (s *webhookAuthenticatorLister) List(selector labels.Selector) (ret []*v1alpha1.WebhookAuthenticator, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.WebhookAuthenticator)) + }) + return ret, err +} + +// Get retrieves the WebhookAuthenticator from the index for a given name. +func (s *webhookAuthenticatorLister) Get(name string) (*v1alpha1.WebhookAuthenticator, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("webhookauthenticator"), name) + } + return obj.(*v1alpha1.WebhookAuthenticator), nil +} diff --git a/generated/1.24/client/concierge/listers/config/v1alpha1/credentialissuer.go b/generated/1.24/client/concierge/listers/config/v1alpha1/credentialissuer.go new file mode 100644 index 00000000..7da49f25 --- /dev/null +++ b/generated/1.24/client/concierge/listers/config/v1alpha1/credentialissuer.go @@ -0,0 +1,55 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/concierge/config/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// CredentialIssuerLister helps list CredentialIssuers. +// All objects returned here must be treated as read-only. +type CredentialIssuerLister interface { + // List lists all CredentialIssuers in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.CredentialIssuer, err error) + // Get retrieves the CredentialIssuer from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.CredentialIssuer, error) + CredentialIssuerListerExpansion +} + +// credentialIssuerLister implements the CredentialIssuerLister interface. +type credentialIssuerLister struct { + indexer cache.Indexer +} + +// NewCredentialIssuerLister returns a new CredentialIssuerLister. +func NewCredentialIssuerLister(indexer cache.Indexer) CredentialIssuerLister { + return &credentialIssuerLister{indexer: indexer} +} + +// List lists all CredentialIssuers in the indexer. +func (s *credentialIssuerLister) List(selector labels.Selector) (ret []*v1alpha1.CredentialIssuer, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.CredentialIssuer)) + }) + return ret, err +} + +// Get retrieves the CredentialIssuer from the index for a given name. +func (s *credentialIssuerLister) Get(name string) (*v1alpha1.CredentialIssuer, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("credentialissuer"), name) + } + return obj.(*v1alpha1.CredentialIssuer), nil +} diff --git a/generated/1.24/client/concierge/listers/config/v1alpha1/expansion_generated.go b/generated/1.24/client/concierge/listers/config/v1alpha1/expansion_generated.go new file mode 100644 index 00000000..7212c891 --- /dev/null +++ b/generated/1.24/client/concierge/listers/config/v1alpha1/expansion_generated.go @@ -0,0 +1,10 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// CredentialIssuerListerExpansion allows custom methods to be added to +// CredentialIssuerLister. +type CredentialIssuerListerExpansion interface{} diff --git a/generated/1.24/client/go.mod b/generated/1.24/client/go.mod new file mode 100644 index 00000000..db4bffaa --- /dev/null +++ b/generated/1.24/client/go.mod @@ -0,0 +1,12 @@ +// This go.mod file is generated by ./hack/codegen.sh. +module go.pinniped.dev/generated/1.24/client + +go 1.13 + +require ( + go.pinniped.dev/generated/1.24/apis v0.0.0 + k8s.io/apimachinery v0.24.1 + k8s.io/client-go v0.24.1 +) + +replace go.pinniped.dev/generated/1.24/apis => ../apis diff --git a/generated/1.24/client/go.sum b/generated/1.24/client/go.sum new file mode 100644 index 00000000..6abff42b --- /dev/null +++ b/generated/1.24/client/go.sum @@ -0,0 +1,638 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/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/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-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.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.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/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/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-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-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.24.1 h1:BjCMRDcyEYz03joa3K1+rbshwh1Ay6oB53+iUx2H8UY= +k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= +k8s.io/apimachinery v0.24.1 h1:ShD4aDxTQKN5zNf8K1RQ2u98ELLdIW7jEnlO9uAMX/I= +k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/client-go v0.24.1 h1:w1hNdI9PFrzu3OlovVeTnf4oHDt+FJLd9Ndluvnb42E= +k8s.io/client-go v0.24.1/go.mod h1:f1kIDqcEYmwXS/vTbbhopMUbhKp2JhOeVTfxgaCIlF8= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +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.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= +k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU= +k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/generated/1.24/client/supervisor/clientset/versioned/clientset.go b/generated/1.24/client/supervisor/clientset/versioned/clientset.go new file mode 100644 index 00000000..39ee1be5 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/clientset.go @@ -0,0 +1,121 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + "fmt" + "net/http" + + configv1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + ConfigV1alpha1() configv1alpha1.ConfigV1alpha1Interface + IDPV1alpha1() idpv1alpha1.IDPV1alpha1Interface +} + +// Clientset contains the clients for groups. Each group has exactly one +// version included in a Clientset. +type Clientset struct { + *discovery.DiscoveryClient + configV1alpha1 *configv1alpha1.ConfigV1alpha1Client + iDPV1alpha1 *idpv1alpha1.IDPV1alpha1Client +} + +// ConfigV1alpha1 retrieves the ConfigV1alpha1Client +func (c *Clientset) ConfigV1alpha1() configv1alpha1.ConfigV1alpha1Interface { + return c.configV1alpha1 +} + +// IDPV1alpha1 retrieves the IDPV1alpha1Client +func (c *Clientset) IDPV1alpha1() idpv1alpha1.IDPV1alpha1Interface { + return c.iDPV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.configV1alpha1, err = configv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + cs.iDPV1alpha1, err = idpv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.configV1alpha1 = configv1alpha1.New(c) + cs.iDPV1alpha1 = idpv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/doc.go b/generated/1.24/client/supervisor/clientset/versioned/doc.go new file mode 100644 index 00000000..5dc02e6e --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated clientset. +package versioned diff --git a/generated/1.24/client/supervisor/clientset/versioned/fake/clientset_generated.go b/generated/1.24/client/supervisor/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 00000000..f613b900 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,79 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + configv1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1" + fakeconfigv1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake" + idpv1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1" + fakeidpv1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// ConfigV1alpha1 retrieves the ConfigV1alpha1Client +func (c *Clientset) ConfigV1alpha1() configv1alpha1.ConfigV1alpha1Interface { + return &fakeconfigv1alpha1.FakeConfigV1alpha1{Fake: &c.Fake} +} + +// IDPV1alpha1 retrieves the IDPV1alpha1Client +func (c *Clientset) IDPV1alpha1() idpv1alpha1.IDPV1alpha1Interface { + return &fakeidpv1alpha1.FakeIDPV1alpha1{Fake: &c.Fake} +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/fake/doc.go b/generated/1.24/client/supervisor/clientset/versioned/fake/doc.go new file mode 100644 index 00000000..7c9538fd --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/generated/1.24/client/supervisor/clientset/versioned/fake/register.go b/generated/1.24/client/supervisor/clientset/versioned/fake/register.go new file mode 100644 index 00000000..e74fd77e --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/fake/register.go @@ -0,0 +1,45 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + configv1alpha1.AddToScheme, + idpv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/scheme/doc.go b/generated/1.24/client/supervisor/clientset/versioned/scheme/doc.go new file mode 100644 index 00000000..cc02f1d3 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/scheme/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/generated/1.24/client/supervisor/clientset/versioned/scheme/register.go b/generated/1.24/client/supervisor/clientset/versioned/scheme/register.go new file mode 100644 index 00000000..4e2cb90f --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/scheme/register.go @@ -0,0 +1,45 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + configv1alpha1.AddToScheme, + idpv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/config_client.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/config_client.go new file mode 100644 index 00000000..dc9ff4c2 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/config_client.go @@ -0,0 +1,94 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type ConfigV1alpha1Interface interface { + RESTClient() rest.Interface + FederationDomainsGetter +} + +// ConfigV1alpha1Client is used to interact with features provided by the config.supervisor.pinniped.dev group. +type ConfigV1alpha1Client struct { + restClient rest.Interface +} + +func (c *ConfigV1alpha1Client) FederationDomains(namespace string) FederationDomainInterface { + return newFederationDomains(c, namespace) +} + +// NewForConfig creates a new ConfigV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*ConfigV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new ConfigV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*ConfigV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &ConfigV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new ConfigV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *ConfigV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new ConfigV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *ConfigV1alpha1Client { + return &ConfigV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *ConfigV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/doc.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/doc.go new file mode 100644 index 00000000..e7a470b6 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/doc.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/doc.go new file mode 100644 index 00000000..7906901b --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go new file mode 100644 index 00000000..19460208 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_config_client.go @@ -0,0 +1,27 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeConfigV1alpha1 struct { + *testing.Fake +} + +func (c *FakeConfigV1alpha1) FederationDomains(namespace string) v1alpha1.FederationDomainInterface { + return &FakeFederationDomains{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeConfigV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_federationdomain.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_federationdomain.go new file mode 100644 index 00000000..ff30701b --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/fake/fake_federationdomain.go @@ -0,0 +1,129 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeFederationDomains implements FederationDomainInterface +type FakeFederationDomains struct { + Fake *FakeConfigV1alpha1 + ns string +} + +var federationdomainsResource = schema.GroupVersionResource{Group: "config.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "federationdomains"} + +var federationdomainsKind = schema.GroupVersionKind{Group: "config.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "FederationDomain"} + +// Get takes name of the federationDomain, and returns the corresponding federationDomain object, and an error if there is any. +func (c *FakeFederationDomains) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.FederationDomain, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(federationdomainsResource, c.ns, name), &v1alpha1.FederationDomain{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FederationDomain), err +} + +// List takes label and field selectors, and returns the list of FederationDomains that match those selectors. +func (c *FakeFederationDomains) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.FederationDomainList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(federationdomainsResource, federationdomainsKind, c.ns, opts), &v1alpha1.FederationDomainList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.FederationDomainList{ListMeta: obj.(*v1alpha1.FederationDomainList).ListMeta} + for _, item := range obj.(*v1alpha1.FederationDomainList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested federationDomains. +func (c *FakeFederationDomains) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(federationdomainsResource, c.ns, opts)) + +} + +// Create takes the representation of a federationDomain and creates it. Returns the server's representation of the federationDomain, and an error, if there is any. +func (c *FakeFederationDomains) Create(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.CreateOptions) (result *v1alpha1.FederationDomain, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(federationdomainsResource, c.ns, federationDomain), &v1alpha1.FederationDomain{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FederationDomain), err +} + +// Update takes the representation of a federationDomain and updates it. Returns the server's representation of the federationDomain, and an error, if there is any. +func (c *FakeFederationDomains) Update(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.UpdateOptions) (result *v1alpha1.FederationDomain, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(federationdomainsResource, c.ns, federationDomain), &v1alpha1.FederationDomain{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FederationDomain), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeFederationDomains) UpdateStatus(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.UpdateOptions) (*v1alpha1.FederationDomain, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(federationdomainsResource, "status", c.ns, federationDomain), &v1alpha1.FederationDomain{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FederationDomain), err +} + +// Delete takes name of the federationDomain and deletes it. Returns an error if one occurs. +func (c *FakeFederationDomains) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(federationdomainsResource, c.ns, name, opts), &v1alpha1.FederationDomain{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeFederationDomains) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(federationdomainsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.FederationDomainList{}) + return err +} + +// Patch applies the patch and returns the patched federationDomain. +func (c *FakeFederationDomains) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.FederationDomain, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(federationdomainsResource, c.ns, name, pt, data, subresources...), &v1alpha1.FederationDomain{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FederationDomain), err +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/federationdomain.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/federationdomain.go new file mode 100644 index 00000000..0346a50d --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/federationdomain.go @@ -0,0 +1,182 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// FederationDomainsGetter has a method to return a FederationDomainInterface. +// A group's client should implement this interface. +type FederationDomainsGetter interface { + FederationDomains(namespace string) FederationDomainInterface +} + +// FederationDomainInterface has methods to work with FederationDomain resources. +type FederationDomainInterface interface { + Create(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.CreateOptions) (*v1alpha1.FederationDomain, error) + Update(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.UpdateOptions) (*v1alpha1.FederationDomain, error) + UpdateStatus(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.UpdateOptions) (*v1alpha1.FederationDomain, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.FederationDomain, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.FederationDomainList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.FederationDomain, err error) + FederationDomainExpansion +} + +// federationDomains implements FederationDomainInterface +type federationDomains struct { + client rest.Interface + ns string +} + +// newFederationDomains returns a FederationDomains +func newFederationDomains(c *ConfigV1alpha1Client, namespace string) *federationDomains { + return &federationDomains{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the federationDomain, and returns the corresponding federationDomain object, and an error if there is any. +func (c *federationDomains) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.FederationDomain, err error) { + result = &v1alpha1.FederationDomain{} + err = c.client.Get(). + Namespace(c.ns). + Resource("federationdomains"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of FederationDomains that match those selectors. +func (c *federationDomains) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.FederationDomainList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.FederationDomainList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("federationdomains"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested federationDomains. +func (c *federationDomains) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("federationdomains"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a federationDomain and creates it. Returns the server's representation of the federationDomain, and an error, if there is any. +func (c *federationDomains) Create(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.CreateOptions) (result *v1alpha1.FederationDomain, err error) { + result = &v1alpha1.FederationDomain{} + err = c.client.Post(). + Namespace(c.ns). + Resource("federationdomains"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(federationDomain). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a federationDomain and updates it. Returns the server's representation of the federationDomain, and an error, if there is any. +func (c *federationDomains) Update(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.UpdateOptions) (result *v1alpha1.FederationDomain, err error) { + result = &v1alpha1.FederationDomain{} + err = c.client.Put(). + Namespace(c.ns). + Resource("federationdomains"). + Name(federationDomain.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(federationDomain). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *federationDomains) UpdateStatus(ctx context.Context, federationDomain *v1alpha1.FederationDomain, opts v1.UpdateOptions) (result *v1alpha1.FederationDomain, err error) { + result = &v1alpha1.FederationDomain{} + err = c.client.Put(). + Namespace(c.ns). + Resource("federationdomains"). + Name(federationDomain.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(federationDomain). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the federationDomain and deletes it. Returns an error if one occurs. +func (c *federationDomains) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("federationdomains"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *federationDomains) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("federationdomains"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched federationDomain. +func (c *federationDomains) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.FederationDomain, err error) { + result = &v1alpha1.FederationDomain{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("federationdomains"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/generated_expansion.go b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..ba9c9173 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/config/v1alpha1/generated_expansion.go @@ -0,0 +1,8 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type FederationDomainExpansion interface{} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 00000000..0c19f4a6 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ActiveDirectoryIdentityProvidersGetter has a method to return a ActiveDirectoryIdentityProviderInterface. +// A group's client should implement this interface. +type ActiveDirectoryIdentityProvidersGetter interface { + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface +} + +// ActiveDirectoryIdentityProviderInterface has methods to work with ActiveDirectoryIdentityProvider resources. +type ActiveDirectoryIdentityProviderInterface interface { + Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ActiveDirectoryIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) + ActiveDirectoryIdentityProviderExpansion +} + +// activeDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type activeDirectoryIdentityProviders struct { + client rest.Interface + ns string +} + +// newActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviders +func newActiveDirectoryIdentityProviders(c *IDPV1alpha1Client, namespace string) *activeDirectoryIdentityProviders { + return &activeDirectoryIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *activeDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *activeDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ActiveDirectoryIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *activeDirectoryIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *activeDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *activeDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *activeDirectoryIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *activeDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/doc.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/doc.go new file mode 100644 index 00000000..e7a470b6 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/doc.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/doc.go new file mode 100644 index 00000000..7906901b --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go new file mode 100644 index 00000000..92a47701 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeActiveDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type FakeActiveDirectoryIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var activedirectoryidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "activedirectoryidentityproviders"} + +var activedirectoryidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "ActiveDirectoryIdentityProvider"} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *FakeActiveDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(activedirectoryidentityprovidersResource, activedirectoryidentityprovidersKind, c.ns, opts), &v1alpha1.ActiveDirectoryIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ActiveDirectoryIdentityProviderList{ListMeta: obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *FakeActiveDirectoryIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(activedirectoryidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeActiveDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(activedirectoryidentityprovidersResource, "status", c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeActiveDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(activedirectoryidentityprovidersResource, c.ns, name, opts), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeActiveDirectoryIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(activedirectoryidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ActiveDirectoryIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *FakeActiveDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(activedirectoryidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go new file mode 100644 index 00000000..ec457e84 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -0,0 +1,35 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeIDPV1alpha1 struct { + *testing.Fake +} + +func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1alpha1.ActiveDirectoryIdentityProviderInterface { + return &FakeActiveDirectoryIdentityProviders{c, namespace} +} + +func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { + return &FakeLDAPIdentityProviders{c, namespace} +} + +func (c *FakeIDPV1alpha1) OIDCIdentityProviders(namespace string) v1alpha1.OIDCIdentityProviderInterface { + return &FakeOIDCIdentityProviders{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeIDPV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go new file mode 100644 index 00000000..b303c3c0 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLDAPIdentityProviders implements LDAPIdentityProviderInterface +type FakeLDAPIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var ldapidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "ldapidentityproviders"} + +var ldapidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "LDAPIdentityProvider"} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *FakeLDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *FakeLDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ldapidentityprovidersResource, ldapidentityprovidersKind, c.ns, opts), &v1alpha1.LDAPIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.LDAPIdentityProviderList{ListMeta: obj.(*v1alpha1.LDAPIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.LDAPIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *FakeLDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ldapidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeLDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(ldapidentityprovidersResource, "status", c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeLDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(ldapidentityprovidersResource, c.ns, name, opts), &v1alpha1.LDAPIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ldapidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.LDAPIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *FakeLDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ldapidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_oidcidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_oidcidentityprovider.go new file mode 100644 index 00000000..bff624c9 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_oidcidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeOIDCIdentityProviders implements OIDCIdentityProviderInterface +type FakeOIDCIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var oidcidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "oidcidentityproviders"} + +var oidcidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "OIDCIdentityProvider"} + +// Get takes name of the oIDCIdentityProvider, and returns the corresponding oIDCIdentityProvider object, and an error if there is any. +func (c *FakeOIDCIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(oidcidentityprovidersResource, c.ns, name), &v1alpha1.OIDCIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.OIDCIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of OIDCIdentityProviders that match those selectors. +func (c *FakeOIDCIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.OIDCIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(oidcidentityprovidersResource, oidcidentityprovidersKind, c.ns, opts), &v1alpha1.OIDCIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.OIDCIdentityProviderList{ListMeta: obj.(*v1alpha1.OIDCIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.OIDCIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested oIDCIdentityProviders. +func (c *FakeOIDCIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(oidcidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a oIDCIdentityProvider and creates it. Returns the server's representation of the oIDCIdentityProvider, and an error, if there is any. +func (c *FakeOIDCIdentityProviders) Create(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(oidcidentityprovidersResource, c.ns, oIDCIdentityProvider), &v1alpha1.OIDCIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.OIDCIdentityProvider), err +} + +// Update takes the representation of a oIDCIdentityProvider and updates it. Returns the server's representation of the oIDCIdentityProvider, and an error, if there is any. +func (c *FakeOIDCIdentityProviders) Update(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(oidcidentityprovidersResource, c.ns, oIDCIdentityProvider), &v1alpha1.OIDCIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.OIDCIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeOIDCIdentityProviders) UpdateStatus(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.OIDCIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(oidcidentityprovidersResource, "status", c.ns, oIDCIdentityProvider), &v1alpha1.OIDCIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.OIDCIdentityProvider), err +} + +// Delete takes name of the oIDCIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeOIDCIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(oidcidentityprovidersResource, c.ns, name, opts), &v1alpha1.OIDCIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeOIDCIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(oidcidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.OIDCIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched oIDCIdentityProvider. +func (c *FakeOIDCIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.OIDCIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(oidcidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.OIDCIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.OIDCIdentityProvider), err +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go new file mode 100644 index 00000000..cbefaca4 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -0,0 +1,12 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type ActiveDirectoryIdentityProviderExpansion interface{} + +type LDAPIdentityProviderExpansion interface{} + +type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go new file mode 100644 index 00000000..efaa9879 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -0,0 +1,104 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type IDPV1alpha1Interface interface { + RESTClient() rest.Interface + ActiveDirectoryIdentityProvidersGetter + LDAPIdentityProvidersGetter + OIDCIdentityProvidersGetter +} + +// IDPV1alpha1Client is used to interact with features provided by the idp.supervisor.pinniped.dev group. +type IDPV1alpha1Client struct { + restClient rest.Interface +} + +func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface { + return newActiveDirectoryIdentityProviders(c, namespace) +} + +func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { + return newLDAPIdentityProviders(c, namespace) +} + +func (c *IDPV1alpha1Client) OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface { + return newOIDCIdentityProviders(c, namespace) +} + +// NewForConfig creates a new IDPV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*IDPV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new IDPV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*IDPV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &IDPV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new IDPV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *IDPV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new IDPV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *IDPV1alpha1Client { + return &IDPV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *IDPV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..f03afbca --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// LDAPIdentityProvidersGetter has a method to return a LDAPIdentityProviderInterface. +// A group's client should implement this interface. +type LDAPIdentityProvidersGetter interface { + LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface +} + +// LDAPIdentityProviderInterface has methods to work with LDAPIdentityProvider resources. +type LDAPIdentityProviderInterface interface { + Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.LDAPIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.LDAPIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) + LDAPIdentityProviderExpansion +} + +// lDAPIdentityProviders implements LDAPIdentityProviderInterface +type lDAPIdentityProviders struct { + client rest.Interface + ns string +} + +// newLDAPIdentityProviders returns a LDAPIdentityProviders +func newLDAPIdentityProviders(c *IDPV1alpha1Client, namespace string) *lDAPIdentityProviders { + return &lDAPIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *lDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *lDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.LDAPIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *lDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *lDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *lDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *lDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *lDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/oidcidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/oidcidentityprovider.go new file mode 100644 index 00000000..e96cd113 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/oidcidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// OIDCIdentityProvidersGetter has a method to return a OIDCIdentityProviderInterface. +// A group's client should implement this interface. +type OIDCIdentityProvidersGetter interface { + OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface +} + +// OIDCIdentityProviderInterface has methods to work with OIDCIdentityProvider resources. +type OIDCIdentityProviderInterface interface { + Create(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.CreateOptions) (*v1alpha1.OIDCIdentityProvider, error) + Update(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.OIDCIdentityProvider, error) + UpdateStatus(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.OIDCIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.OIDCIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.OIDCIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.OIDCIdentityProvider, err error) + OIDCIdentityProviderExpansion +} + +// oIDCIdentityProviders implements OIDCIdentityProviderInterface +type oIDCIdentityProviders struct { + client rest.Interface + ns string +} + +// newOIDCIdentityProviders returns a OIDCIdentityProviders +func newOIDCIdentityProviders(c *IDPV1alpha1Client, namespace string) *oIDCIdentityProviders { + return &oIDCIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the oIDCIdentityProvider, and returns the corresponding oIDCIdentityProvider object, and an error if there is any. +func (c *oIDCIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + result = &v1alpha1.OIDCIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of OIDCIdentityProviders that match those selectors. +func (c *oIDCIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.OIDCIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.OIDCIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested oIDCIdentityProviders. +func (c *oIDCIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a oIDCIdentityProvider and creates it. Returns the server's representation of the oIDCIdentityProvider, and an error, if there is any. +func (c *oIDCIdentityProviders) Create(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + result = &v1alpha1.OIDCIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(oIDCIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a oIDCIdentityProvider and updates it. Returns the server's representation of the oIDCIdentityProvider, and an error, if there is any. +func (c *oIDCIdentityProviders) Update(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + result = &v1alpha1.OIDCIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + Name(oIDCIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(oIDCIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *oIDCIdentityProviders) UpdateStatus(ctx context.Context, oIDCIdentityProvider *v1alpha1.OIDCIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.OIDCIdentityProvider, err error) { + result = &v1alpha1.OIDCIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + Name(oIDCIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(oIDCIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the oIDCIdentityProvider and deletes it. Returns an error if one occurs. +func (c *oIDCIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *oIDCIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("oidcidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched oIDCIdentityProvider. +func (c *oIDCIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.OIDCIdentityProvider, err error) { + result = &v1alpha1.OIDCIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("oidcidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/config/interface.go b/generated/1.24/client/supervisor/informers/externalversions/config/interface.go new file mode 100644 index 00000000..a150ad7d --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/config/interface.go @@ -0,0 +1,33 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package config + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/federationdomain.go b/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/federationdomain.go new file mode 100644 index 00000000..250c8fc4 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/federationdomain.go @@ -0,0 +1,77 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + configv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/listers/config/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// FederationDomainInformer provides access to a shared informer and lister for +// FederationDomains. +type FederationDomainInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.FederationDomainLister +} + +type federationDomainInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewFederationDomainInformer constructs a new informer for FederationDomain type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFederationDomainInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredFederationDomainInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredFederationDomainInformer constructs a new informer for FederationDomain type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredFederationDomainInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ConfigV1alpha1().FederationDomains(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ConfigV1alpha1().FederationDomains(namespace).Watch(context.TODO(), options) + }, + }, + &configv1alpha1.FederationDomain{}, + resyncPeriod, + indexers, + ) +} + +func (f *federationDomainInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredFederationDomainInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *federationDomainInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&configv1alpha1.FederationDomain{}, f.defaultInformer) +} + +func (f *federationDomainInformer) Lister() v1alpha1.FederationDomainLister { + return v1alpha1.NewFederationDomainLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/interface.go b/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/interface.go new file mode 100644 index 00000000..37374c24 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/config/v1alpha1/interface.go @@ -0,0 +1,32 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // FederationDomains returns a FederationDomainInformer. + FederationDomains() FederationDomainInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// FederationDomains returns a FederationDomainInformer. +func (v *version) FederationDomains() FederationDomainInformer { + return &federationDomainInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/factory.go b/generated/1.24/client/supervisor/informers/externalversions/factory.go new file mode 100644 index 00000000..cd409f8c --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/factory.go @@ -0,0 +1,173 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + config "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/config" + idp "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/idp" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +// Start initializes all requested informers. +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InternalInformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + Config() config.Interface + IDP() idp.Interface +} + +func (f *sharedInformerFactory) Config() config.Interface { + return config.New(f, f.namespace, f.tweakListOptions) +} + +func (f *sharedInformerFactory) IDP() idp.Interface { + return idp.New(f, f.namespace, f.tweakListOptions) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/generic.go b/generated/1.24/client/supervisor/informers/externalversions/generic.go new file mode 100644 index 00000000..667b7dfe --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/generic.go @@ -0,0 +1,58 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + "fmt" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=config.supervisor.pinniped.dev, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("federationdomains"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().FederationDomains().Informer()}, nil + + // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 + case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().OIDCIdentityProviders().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/interface.go b/generated/1.24/client/supervisor/informers/externalversions/idp/interface.go new file mode 100644 index 00000000..129c1bc8 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/interface.go @@ -0,0 +1,33 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package idp + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 00000000..abd581a2 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ActiveDirectoryIdentityProviderInformer provides access to a shared informer and lister for +// ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ActiveDirectoryIdentityProviderLister +} + +type activeDirectoryIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *activeDirectoryIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *activeDirectoryIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.ActiveDirectoryIdentityProvider{}, f.defaultInformer) +} + +func (f *activeDirectoryIdentityProviderInformer) Lister() v1alpha1.ActiveDirectoryIdentityProviderLister { + return v1alpha1.NewActiveDirectoryIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go new file mode 100644 index 00000000..5d88b4f0 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -0,0 +1,46 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. + ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. + LDAPIdentityProviders() LDAPIdentityProviderInformer + // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. + OIDCIdentityProviders() OIDCIdentityProviderInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. +func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer { + return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// LDAPIdentityProviders returns a LDAPIdentityProviderInformer. +func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { + return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// OIDCIdentityProviders returns a OIDCIdentityProviderInformer. +func (v *version) OIDCIdentityProviders() OIDCIdentityProviderInformer { + return &oIDCIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..d316b72a --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderInformer provides access to a shared informer and lister for +// LDAPIdentityProviders. +type LDAPIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.LDAPIdentityProviderLister +} + +type lDAPIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.LDAPIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *lDAPIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *lDAPIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.LDAPIdentityProvider{}, f.defaultInformer) +} + +func (f *lDAPIdentityProviderInformer) Lister() v1alpha1.LDAPIdentityProviderLister { + return v1alpha1.NewLDAPIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/oidcidentityprovider.go b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/oidcidentityprovider.go new file mode 100644 index 00000000..aa9361c0 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/oidcidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// OIDCIdentityProviderInformer provides access to a shared informer and lister for +// OIDCIdentityProviders. +type OIDCIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.OIDCIdentityProviderLister +} + +type oIDCIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewOIDCIdentityProviderInformer constructs a new informer for OIDCIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewOIDCIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredOIDCIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredOIDCIdentityProviderInformer constructs a new informer for OIDCIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredOIDCIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().OIDCIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().OIDCIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.OIDCIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *oIDCIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredOIDCIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *oIDCIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.OIDCIdentityProvider{}, f.defaultInformer) +} + +func (f *oIDCIdentityProviderInformer) Lister() v1alpha1.OIDCIdentityProviderLister { + return v1alpha1.NewOIDCIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces/factory_interfaces.go b/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 00000000..2b440217 --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,27 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/generated/1.24/client/supervisor/listers/config/v1alpha1/expansion_generated.go b/generated/1.24/client/supervisor/listers/config/v1alpha1/expansion_generated.go new file mode 100644 index 00000000..d59892c4 --- /dev/null +++ b/generated/1.24/client/supervisor/listers/config/v1alpha1/expansion_generated.go @@ -0,0 +1,14 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// FederationDomainListerExpansion allows custom methods to be added to +// FederationDomainLister. +type FederationDomainListerExpansion interface{} + +// FederationDomainNamespaceListerExpansion allows custom methods to be added to +// FederationDomainNamespaceLister. +type FederationDomainNamespaceListerExpansion interface{} diff --git a/generated/1.24/client/supervisor/listers/config/v1alpha1/federationdomain.go b/generated/1.24/client/supervisor/listers/config/v1alpha1/federationdomain.go new file mode 100644 index 00000000..6b6b3dc7 --- /dev/null +++ b/generated/1.24/client/supervisor/listers/config/v1alpha1/federationdomain.go @@ -0,0 +1,86 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/config/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// FederationDomainLister helps list FederationDomains. +// All objects returned here must be treated as read-only. +type FederationDomainLister interface { + // List lists all FederationDomains in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.FederationDomain, err error) + // FederationDomains returns an object that can list and get FederationDomains. + FederationDomains(namespace string) FederationDomainNamespaceLister + FederationDomainListerExpansion +} + +// federationDomainLister implements the FederationDomainLister interface. +type federationDomainLister struct { + indexer cache.Indexer +} + +// NewFederationDomainLister returns a new FederationDomainLister. +func NewFederationDomainLister(indexer cache.Indexer) FederationDomainLister { + return &federationDomainLister{indexer: indexer} +} + +// List lists all FederationDomains in the indexer. +func (s *federationDomainLister) List(selector labels.Selector) (ret []*v1alpha1.FederationDomain, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.FederationDomain)) + }) + return ret, err +} + +// FederationDomains returns an object that can list and get FederationDomains. +func (s *federationDomainLister) FederationDomains(namespace string) FederationDomainNamespaceLister { + return federationDomainNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// FederationDomainNamespaceLister helps list and get FederationDomains. +// All objects returned here must be treated as read-only. +type FederationDomainNamespaceLister interface { + // List lists all FederationDomains in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.FederationDomain, err error) + // Get retrieves the FederationDomain from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.FederationDomain, error) + FederationDomainNamespaceListerExpansion +} + +// federationDomainNamespaceLister implements the FederationDomainNamespaceLister +// interface. +type federationDomainNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all FederationDomains in the indexer for a given namespace. +func (s federationDomainNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.FederationDomain, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.FederationDomain)) + }) + return ret, err +} + +// Get retrieves the FederationDomain from the indexer for a given namespace and name. +func (s federationDomainNamespaceLister) Get(name string) (*v1alpha1.FederationDomain, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("federationdomain"), name) + } + return obj.(*v1alpha1.FederationDomain), nil +} diff --git a/generated/1.24/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.24/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 00000000..79b711dc --- /dev/null +++ b/generated/1.24/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ActiveDirectoryIdentityProviderLister helps list ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister + ActiveDirectoryIdentityProviderListerExpansion +} + +// activeDirectoryIdentityProviderLister implements the ActiveDirectoryIdentityProviderLister interface. +type activeDirectoryIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewActiveDirectoryIdentityProviderLister returns a new ActiveDirectoryIdentityProviderLister. +func NewActiveDirectoryIdentityProviderLister(indexer cache.Indexer) ActiveDirectoryIdentityProviderLister { + return &activeDirectoryIdentityProviderLister{indexer: indexer} +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer. +func (s *activeDirectoryIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. +func (s *activeDirectoryIdentityProviderLister) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister { + return activeDirectoryIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ActiveDirectoryIdentityProviderNamespaceLister helps list and get ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderNamespaceLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + ActiveDirectoryIdentityProviderNamespaceListerExpansion +} + +// activeDirectoryIdentityProviderNamespaceLister implements the ActiveDirectoryIdentityProviderNamespaceLister +// interface. +type activeDirectoryIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. +func (s activeDirectoryIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. +func (s activeDirectoryIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("activedirectoryidentityprovider"), name) + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), nil +} diff --git a/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go new file mode 100644 index 00000000..d79cd0fe --- /dev/null +++ b/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -0,0 +1,30 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// ActiveDirectoryIdentityProviderListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderLister. +type ActiveDirectoryIdentityProviderListerExpansion interface{} + +// ActiveDirectoryIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderNamespaceLister. +type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} + +// LDAPIdentityProviderListerExpansion allows custom methods to be added to +// LDAPIdentityProviderLister. +type LDAPIdentityProviderListerExpansion interface{} + +// LDAPIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// LDAPIdentityProviderNamespaceLister. +type LDAPIdentityProviderNamespaceListerExpansion interface{} + +// OIDCIdentityProviderListerExpansion allows custom methods to be added to +// OIDCIdentityProviderLister. +type OIDCIdentityProviderListerExpansion interface{} + +// OIDCIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// OIDCIdentityProviderNamespaceLister. +type OIDCIdentityProviderNamespaceListerExpansion interface{} diff --git a/generated/1.24/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go b/generated/1.24/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..69002d62 --- /dev/null +++ b/generated/1.24/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderLister helps list LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderLister interface { + // List lists all LDAPIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. + LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister + LDAPIdentityProviderListerExpansion +} + +// lDAPIdentityProviderLister implements the LDAPIdentityProviderLister interface. +type lDAPIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewLDAPIdentityProviderLister returns a new LDAPIdentityProviderLister. +func NewLDAPIdentityProviderLister(indexer cache.Indexer) LDAPIdentityProviderLister { + return &lDAPIdentityProviderLister{indexer: indexer} +} + +// List lists all LDAPIdentityProviders in the indexer. +func (s *lDAPIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. +func (s *lDAPIdentityProviderLister) LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister { + return lDAPIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// LDAPIdentityProviderNamespaceLister helps list and get LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderNamespaceLister interface { + // List lists all LDAPIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.LDAPIdentityProvider, error) + LDAPIdentityProviderNamespaceListerExpansion +} + +// lDAPIdentityProviderNamespaceLister implements the LDAPIdentityProviderNamespaceLister +// interface. +type lDAPIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all LDAPIdentityProviders in the indexer for a given namespace. +func (s lDAPIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. +func (s lDAPIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.LDAPIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ldapidentityprovider"), name) + } + return obj.(*v1alpha1.LDAPIdentityProvider), nil +} diff --git a/generated/1.24/client/supervisor/listers/idp/v1alpha1/oidcidentityprovider.go b/generated/1.24/client/supervisor/listers/idp/v1alpha1/oidcidentityprovider.go new file mode 100644 index 00000000..78caa2f7 --- /dev/null +++ b/generated/1.24/client/supervisor/listers/idp/v1alpha1/oidcidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// OIDCIdentityProviderLister helps list OIDCIdentityProviders. +// All objects returned here must be treated as read-only. +type OIDCIdentityProviderLister interface { + // List lists all OIDCIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.OIDCIdentityProvider, err error) + // OIDCIdentityProviders returns an object that can list and get OIDCIdentityProviders. + OIDCIdentityProviders(namespace string) OIDCIdentityProviderNamespaceLister + OIDCIdentityProviderListerExpansion +} + +// oIDCIdentityProviderLister implements the OIDCIdentityProviderLister interface. +type oIDCIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewOIDCIdentityProviderLister returns a new OIDCIdentityProviderLister. +func NewOIDCIdentityProviderLister(indexer cache.Indexer) OIDCIdentityProviderLister { + return &oIDCIdentityProviderLister{indexer: indexer} +} + +// List lists all OIDCIdentityProviders in the indexer. +func (s *oIDCIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.OIDCIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.OIDCIdentityProvider)) + }) + return ret, err +} + +// OIDCIdentityProviders returns an object that can list and get OIDCIdentityProviders. +func (s *oIDCIdentityProviderLister) OIDCIdentityProviders(namespace string) OIDCIdentityProviderNamespaceLister { + return oIDCIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// OIDCIdentityProviderNamespaceLister helps list and get OIDCIdentityProviders. +// All objects returned here must be treated as read-only. +type OIDCIdentityProviderNamespaceLister interface { + // List lists all OIDCIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.OIDCIdentityProvider, err error) + // Get retrieves the OIDCIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.OIDCIdentityProvider, error) + OIDCIdentityProviderNamespaceListerExpansion +} + +// oIDCIdentityProviderNamespaceLister implements the OIDCIdentityProviderNamespaceLister +// interface. +type oIDCIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all OIDCIdentityProviders in the indexer for a given namespace. +func (s oIDCIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.OIDCIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.OIDCIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the OIDCIdentityProvider from the indexer for a given namespace and name. +func (s oIDCIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.OIDCIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("oidcidentityprovider"), name) + } + return obj.(*v1alpha1.OIDCIdentityProvider), nil +} diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml new file mode 100644 index 00000000..a1a77773 --- /dev/null +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -0,0 +1,176 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: jwtauthenticators.authentication.concierge.pinniped.dev +spec: + group: authentication.concierge.pinniped.dev + names: + categories: + - pinniped + - pinniped-authenticator + - pinniped-authenticators + kind: JWTAuthenticator + listKind: JWTAuthenticatorList + plural: jwtauthenticators + singular: jwtauthenticator + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.issuer + name: Issuer + type: string + - jsonPath: .spec.audience + name: Audience + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "JWTAuthenticator describes the configuration of a JWT authenticator. + \n Upon receiving a signed JWT, a JWTAuthenticator will performs some validation + on it (e.g., valid signature, existence of claims, etc.) and extract the + username and groups from the token." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the authenticator. + properties: + audience: + description: Audience is the required value of the "aud" JWT claim. + minLength: 1 + type: string + claims: + description: Claims allows customization of the claims that will be + mapped to user identity for Kubernetes access. + properties: + groups: + description: Groups is the name of the claim which should be read + to extract the user's group membership from the JWT token. When + not specified, it will default to "groups". + type: string + username: + description: Username is the name of the claim which should be + read to extract the username from the JWT token. When not specified, + it will default to "username". + type: string + type: object + issuer: + description: Issuer is the OIDC issuer URL that will be used to discover + public signing keys. Issuer is also used to validate the "iss" JWT + claim. + minLength: 1 + pattern: ^https:// + type: string + tls: + description: TLS configuration for communicating with the OIDC provider. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object + required: + - audience + - issuer + type: object + status: + description: Status of the authenticator. + properties: + conditions: + description: Represents the observations of the authenticator's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml new file mode 100644 index 00000000..07c7f1e5 --- /dev/null +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -0,0 +1,149 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: webhookauthenticators.authentication.concierge.pinniped.dev +spec: + group: authentication.concierge.pinniped.dev + names: + categories: + - pinniped + - pinniped-authenticator + - pinniped-authenticators + kind: WebhookAuthenticator + listKind: WebhookAuthenticatorList + plural: webhookauthenticators + singular: webhookauthenticator + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.endpoint + name: Endpoint + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: WebhookAuthenticator describes the configuration of a webhook + authenticator. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the authenticator. + properties: + endpoint: + description: Webhook server endpoint URL. + minLength: 1 + pattern: ^https:// + type: string + tls: + description: TLS configuration. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object + required: + - endpoint + type: object + status: + description: Status of the authenticator. + properties: + conditions: + description: Represents the observations of the authenticator's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml new file mode 100644 index 00000000..faa2b6d3 --- /dev/null +++ b/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -0,0 +1,246 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: credentialissuers.config.concierge.pinniped.dev +spec: + group: config.concierge.pinniped.dev + names: + categories: + - pinniped + kind: CredentialIssuer + listKind: CredentialIssuerList + plural: credentialissuers + singular: credentialissuer + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.impersonationProxy.mode + name: ProxyMode + type: string + - jsonPath: .status.strategies[?(@.status == "Success")].type + name: DefaultStrategy + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: CredentialIssuer describes the configuration and status of the + Pinniped Concierge credential issuer. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec describes the intended configuration of the Concierge. + properties: + impersonationProxy: + description: ImpersonationProxy describes the intended configuration + of the Concierge impersonation proxy. + properties: + externalEndpoint: + 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.type + is \"None\"." + type: string + mode: + description: '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.' + enum: + - auto + - enabled + - disabled + type: string + service: + default: + type: LoadBalancer + description: Service describes the configuration of the Service + provisioned to expose the impersonation proxy to clients. + properties: + annotations: + additionalProperties: + type: string + description: Annotations specifies zero or more key/value + pairs to set as annotations on the provisioned Service. + type: object + loadBalancerIP: + description: LoadBalancerIP specifies the IP address to set + in the spec.loadBalancerIP field of the provisioned Service. + This is not supported on all cloud providers. + maxLength: 255 + minLength: 1 + type: string + type: + default: LoadBalancer + description: "Type specifies the type of Service to provision + for the impersonation proxy. \n If the type is \"None\", + then the \"spec.impersonationProxy.externalEndpoint\" field + must be set to a non-empty value so that the Concierge can + properly advertise the endpoint in the CredentialIssuer's + status." + enum: + - LoadBalancer + - ClusterIP + - None + type: string + type: object + required: + - mode + - service + type: object + required: + - impersonationProxy + type: object + status: + description: CredentialIssuerStatus describes the status of the Concierge. + properties: + kubeConfigInfo: + description: Information needed to form a valid Pinniped-based kubeconfig + using this credential issuer. This field is deprecated and will + be removed in a future version. + properties: + certificateAuthorityData: + description: The K8s API server CA bundle. + minLength: 1 + type: string + server: + description: The K8s API server URL. + minLength: 1 + pattern: ^https://|^http:// + type: string + required: + - certificateAuthorityData + - server + type: object + strategies: + description: List of integration strategies that were attempted by + Pinniped. + items: + description: CredentialIssuerStrategy describes the status of an + integration strategy that was attempted by Pinniped. + properties: + frontend: + description: Frontend describes how clients can connect using + this strategy. + properties: + impersonationProxyInfo: + description: ImpersonationProxyInfo describes the parameters + for the impersonation proxy on this Concierge. This field + is only set when Type is "ImpersonationProxy". + properties: + certificateAuthorityData: + description: CertificateAuthorityData is the base64-encoded + PEM CA bundle of the impersonation proxy. + minLength: 1 + type: string + endpoint: + description: Endpoint is the HTTPS endpoint of the impersonation + proxy. + minLength: 1 + pattern: ^https:// + type: string + required: + - certificateAuthorityData + - endpoint + type: object + tokenCredentialRequestInfo: + description: TokenCredentialRequestAPIInfo describes the + parameters for the TokenCredentialRequest API on this + Concierge. This field is only set when Type is "TokenCredentialRequestAPI". + properties: + certificateAuthorityData: + description: CertificateAuthorityData is the base64-encoded + Kubernetes API server CA bundle. + minLength: 1 + type: string + server: + description: Server is the Kubernetes API server URL. + minLength: 1 + pattern: ^https://|^http:// + type: string + required: + - certificateAuthorityData + - server + type: object + type: + description: Type describes which frontend mechanism clients + can use with a strategy. + enum: + - TokenCredentialRequestAPI + - ImpersonationProxy + type: string + required: + - type + type: object + lastUpdateTime: + description: When the status was last checked. + format: date-time + type: string + message: + description: Human-readable description of the current status. + minLength: 1 + type: string + reason: + description: Reason for the current status. + enum: + - Listening + - Pending + - Disabled + - ErrorDuringSetup + - CouldNotFetchKey + - CouldNotGetClusterInfo + - FetchedKey + type: string + status: + description: Status of the attempted integration strategy. + enum: + - Success + - Error + type: string + type: + description: Type of integration attempted. + enum: + - KubeClusterSigningCertificate + - ImpersonationProxy + type: string + required: + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + required: + - strategies + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml new file mode 100644 index 00000000..71f7370d --- /dev/null +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: federationdomains.config.supervisor.pinniped.dev +spec: + group: config.supervisor.pinniped.dev + names: + categories: + - pinniped + kind: FederationDomain + listKind: FederationDomainList + plural: federationdomains + singular: federationdomain + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.issuer + name: Issuer + type: string + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: FederationDomain describes the configuration of an OIDC provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec of the OIDC provider. + properties: + issuer: + description: "Issuer is the OIDC Provider's issuer, per the OIDC Discovery + Metadata document, as well as the identifier that it will use for + the iss claim in issued JWTs. This field will also be used as the + base URL for any endpoints used by the OIDC Provider (e.g., if your + issuer is https://example.com/foo, then your authorization endpoint + will look like https://example.com/foo/some/path/to/auth/endpoint). + \n See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 + for more information." + minLength: 1 + type: string + tls: + description: TLS configures how this FederationDomain is served over + Transport Layer Security (TLS). + properties: + secretName: + description: "SecretName is an optional name of a Secret in the + same namespace, of type `kubernetes.io/tls`, which contains + the TLS serving certificate for the HTTPS endpoints served by + this FederationDomain. When provided, the TLS Secret named here + must contain keys named `tls.crt` and `tls.key` that contain + the certificate and private key to use for TLS. \n Server Name + Indication (SNI) is an extension to the Transport Layer Security + (TLS) supported by all major browsers. \n SecretName is required + if you would like to use different TLS certificates for issuers + of different hostnames. SNI requests do not include port numbers, + so all issuers with the same DNS hostname must use the same + SecretName value even if they have different port numbers. \n + SecretName is not required when you would like to use only the + HTTP endpoints (e.g. when the HTTP listener is configured to + listen on loopback interfaces or UNIX domain sockets for traffic + from a service mesh sidecar). It is also not required when you + would like all requests to this OIDC Provider's HTTPS endpoints + to use the default TLS certificate, which is configured elsewhere. + \n When your Issuer URL's host is an IP address, then this field + is ignored. SNI does not work for IP addresses." + type: string + type: object + required: + - issuer + type: object + status: + description: Status of the OIDC provider. + properties: + lastUpdateTime: + description: LastUpdateTime holds the time at which the Status was + last updated. It is a pointer to get around some undesirable behavior + with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811). + format: date-time + type: string + message: + description: Message provides human-readable details about the Status. + type: string + secrets: + description: Secrets contains information about this OIDC Provider's + secrets. + properties: + jwks: + description: JWKS holds the name of the corev1.Secret in which + this OIDC Provider's signing/verification keys are stored. If + it is empty, then the signing/verification keys are either unknown + or they don't exist. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateEncryptionKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for encrypting state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + stateSigningKey: + description: StateSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing state parameters + is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + tokenSigningKey: + description: TokenSigningKey holds the name of the corev1.Secret + in which this OIDC Provider's key for signing tokens is stored. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + type: object + status: + description: Status holds an enum that describes the state of this + OIDC Provider. Note that this Status can represent success or failure. + enum: + - Success + - Duplicate + - Invalid + - SameIssuerHostMustUseSameSecret + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml new file mode 100644 index 00000000..15d2a791 --- /dev/null +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -0,0 +1,304 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: ActiveDirectoryIdentityProvider + listKind: ActiveDirectoryIdentityProviderList + plural: activedirectoryidentityproviders + singular: activedirectoryidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ActiveDirectoryIdentityProvider describes the configuration of + an upstream Microsoft Active Directory identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the ActiveDirectory server + to be allowed to perform searches and binds to validate a user's + credentials during a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + Active Directory bind user. This account will be used to perform + LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" + which includes "username" and "password" keys. The username + value should be the full dn (distinguished name) of your bind + account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + The password must be non-empty. + minLength: 1 + type: string + required: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in ActiveDirectory. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each ActiveDirectory entry which was found + as the result of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the Active Directory entries whose value shall become + a group name in the user's list of groups after a successful + authentication. The value of this field is case-sensitive + and must match the case of the attribute name returned by + the ActiveDirectory server in the user's entry. E.g. "cn" + for common name. Distinguished names can be used by specifying + lower-case "dn". Optional. When not specified, this defaults + to a custom field that looks like "sAMAccountName@domain", + where domain is constructed from the domain components of + the group DN. + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + Optional, when not specified it will be based on the result + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for groups. + It may make sense to specify a subtree as a search base if you + wish to exclude some groups for security reasons or to make + searches faster. + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for groups for a user. The + pattern "{}" must occur in the filter at least once and will + be dynamically replaced by the dn (distinguished name) of the + user entry found as a result of the user search. E.g. "member={}" + or "&(objectClass=groupOfNames)(member={})". For more information + about ActiveDirectory filters, see https://ldap.com/ldap-filters. + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + This searches nested groups by default. Note that nested group + search can be slow for some Active Directory servers. To disable + it, you can set the filter to "(&(objectClass=group)(member={})" + type: string + skipGroupRefresh: + description: "The user's group membership is refreshed as they + interact with the supervisor to obtain new credentials (as their + old credentials expire). This allows group membership changes + to be quickly reflected into Kubernetes clusters. Since group + membership is often used to bind authorization policies, it + is important to keep the groups observed in Kubernetes clusters + in-sync with the identity provider. \n In some environments, + frequent group membership queries may result in a significant + performance impact on the identity provider and/or the supervisor. + The best approach to handle performance impacts is to tweak + the group query to be more performant, for example by disabling + nested group search or by using a more targeted group search + base. \n If the group search query cannot be made performant + and you are willing to have group memberships remain static + for approximately a day, then set skipGroupRefresh to true. + \ This is an insecure configuration as authorization policies + that are bound to group membership will not notice if a user + has been removed from a particular group until their next login. + \n This is an experimental feature that may be removed or significantly + altered in the future. Consumers of this configuration should + carefully read all release notes before upgrading to ensure + that the meaning of this field has not changed." + type: boolean + type: object + host: + description: 'Host is the hostname of this Active Directory identity + provider, i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in Active Directory. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the ActiveDirectory entry which was found as the + result of the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + ActiveDirectory entry which whose value shall be used to + uniquely identify the user within this ActiveDirectory provider + after a successful authentication. Optional, when empty + this defaults to "objectGUID". + type: string + username: + description: Username specifies the name of the attribute + in Active Directory entry whose value shall become the username + of the user after a successful authentication. Optional, + when empty this defaults to "userPrincipalName". + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + Optional, when not specified it will be based on the result + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for users. + It may make sense to specify a subtree as a search base if you + wish to exclude some users or to make searches faster. + type: string + filter: + description: Filter is the search filter which should be applied + when searching for users. The pattern "{}" must occur in the + filter at least once and will be dynamically replaced by the + username for which the search is being run. E.g. "mail={}" or + "&(objectClass=person)(uid={})". For more information about + LDAP filters, see https://ldap.com/ldap-filters. Note that the + dn (distinguished name) is not an attribute of an entry, so + "dn={}" cannot be used. Optional. When not specified, the default + will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + This means that the user is a person, is not a computer, the + sAMAccountType is for a normal user account, and is not shown + in advanced view only (which would likely mean its a system + created service account with advanced permissions). Also, either + the sAMAccountName, the userPrincipalName, or the mail attribute + matches the input username. + type: string + type: object + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml new file mode 100644 index 00000000..211a70a1 --- /dev/null +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -0,0 +1,301 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: ldapidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: LDAPIdentityProvider + listKind: LDAPIdentityProviderList + plural: ldapidentityproviders + singular: ldapidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: LDAPIdentityProvider describes the configuration of an upstream + Lightweight Directory Access Protocol (LDAP) identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the LDAP server to be allowed + to perform searches and binds to validate a user's credentials during + a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + LDAP bind user. This account will be used to perform LDAP searches. + The Secret should be of type "kubernetes.io/basic-auth" which + includes "username" and "password" keys. The username value + should be the full dn (distinguished name) of your bind account, + e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password + must be non-empty. + minLength: 1 + type: string + required: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each LDAP entry which was found as the result + of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the LDAP entries whose value shall become a group name + in the user's list of groups after a successful authentication. + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. E.g. "cn" for common name. Distinguished + names can be used by specifying lower-case "dn". Optional. + When not specified, the default will act as if the GroupName + were specified as "dn" (distinguished name). + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + When not specified, no group search will be performed and authenticated + users will not belong to any groups from the LDAP provider. + Also, when not specified, the values of Filter and Attributes + are ignored. + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for groups for a user. The pattern "{}" + must occur in the filter at least once and will be dynamically + replaced by the dn (distinguished name) of the user entry found + as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". + For more information about LDAP filters, see https://ldap.com/ldap-filters. + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as "member={}". + type: string + skipGroupRefresh: + description: "The user's group membership is refreshed as they + interact with the supervisor to obtain new credentials (as their + old credentials expire). This allows group membership changes + to be quickly reflected into Kubernetes clusters. Since group + membership is often used to bind authorization policies, it + is important to keep the groups observed in Kubernetes clusters + in-sync with the identity provider. \n In some environments, + frequent group membership queries may result in a significant + performance impact on the identity provider and/or the supervisor. + The best approach to handle performance impacts is to tweak + the group query to be more performant, for example by disabling + nested group search or by using a more targeted group search + base. \n If the group search query cannot be made performant + and you are willing to have group memberships remain static + for approximately a day, then set skipGroupRefresh to true. + \ This is an insecure configuration as authorization policies + that are bound to group membership will not notice if a user + has been removed from a particular group until their next login. + \n This is an experimental feature that may be removed or significantly + altered in the future. Consumers of this configuration should + carefully read all release notes before upgrading to ensure + that the meaning of this field has not changed." + type: boolean + type: object + host: + description: 'Host is the hostname of this LDAP identity provider, + i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the LDAP entry which was found as the result of + the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + LDAP entry which whose value shall be used to uniquely identify + the user within this LDAP provider after a successful authentication. + E.g. "uidNumber" or "objectGUID". The value of this field + is case-sensitive and must match the case of the attribute + name returned by the LDAP server in the user's entry. Distinguished + names can be used by specifying lower-case "dn". + minLength: 1 + type: string + username: + description: Username specifies the name of the attribute + in the LDAP entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in the user search + filter, although it can be different. E.g. "mail" or "uid" + or "userPrincipalName". The value of this field is case-sensitive + and must match the case of the attribute name returned by + the LDAP server in the user's entry. Distinguished names + can be used by specifying lower-case "dn". When this field + is set to "dn" then the LDAPIdentityProviderUserSearch's + Filter field cannot be blank, since the default value of + "dn={}" would not work. + minLength: 1 + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + minLength: 1 + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for users. The pattern "{}" must occur + in the filter at least once and will be dynamically replaced + by the username for which the search is being run. E.g. "mail={}" + or "&(objectClass=person)(uid={})". For more information about + LDAP filters, see https://ldap.com/ldap-filters. Note that the + dn (distinguished name) is not an attribute of an entry, so + "dn={}" cannot be used. Optional. When not specified, the default + will act as if the Filter were specified as the value from Attributes.Username + appended by "={}". When the Attributes.Username is set to "dn" + then the Filter must be explicitly specified, since the default + value of "dn={}" would not work. + type: string + type: object + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the LDAPIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml new file mode 100644 index 00000000..2b91026a --- /dev/null +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -0,0 +1,328 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: oidcidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: OIDCIdentityProvider + listKind: OIDCIdentityProviderList + plural: oidcidentityproviders + singular: oidcidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.issuer + name: Issuer + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: OIDCIdentityProvider describes the configuration of an upstream + OpenID Connect identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + authorizationConfig: + description: AuthorizationConfig holds information about how to form + the OAuth2 authorization request parameters to be used with this + OIDC identity provider. + properties: + additionalAuthorizeParameters: + description: additionalAuthorizeParameters are extra query parameters + that should be included in the authorize request to your OIDC + provider in the authorization request during an OIDC Authorization + Code Flow. By default, no extra parameters are sent. The standard + parameters that will be sent are "response_type", "scope", "client_id", + "state", "nonce", "code_challenge", "code_challenge_method", + and "redirect_uri". These parameters cannot be included in this + setting. Additionally, the "hd" parameter cannot be included + in this setting at this time. The "hd" parameter is used by + Google's OIDC provider to provide a hint as to which "hosted + domain" the user should use during login. However, Pinniped + does not yet support validating the hosted domain in the resulting + ID token, so it is not yet safe to use this feature of Google's + OIDC provider with Pinniped. This setting does not influence + the parameters sent to the token endpoint in the Resource Owner + Password Credentials Grant. The Pinniped Supervisor requires + that your OIDC provider returns refresh tokens to the Supervisor + from the authorization flows. Some OIDC providers may require + a certain value for the "prompt" parameter in order to properly + request refresh tokens. See the documentation of your OIDC provider's + authorization endpoint for its requirements for what to include + in the request in order to receive a refresh token in the response, + if anything. If your provider requires the prompt parameter + to request a refresh token, then include it here. Also note + that most providers also require a certain scope to be requested + in order to receive refresh tokens. See the additionalScopes + setting for more information about using scopes to request refresh + tokens. + items: + description: Parameter is a key/value pair which represents + a parameter in an HTTP request. + properties: + name: + description: The name of the parameter. Required. + minLength: 1 + type: string + value: + description: The value of the parameter. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + additionalScopes: + description: 'additionalScopes are the additional scopes that + will be requested from your OIDC provider in the authorization + request during an OIDC Authorization Code Flow and in the token + request during a Resource Owner Password Credentials Grant. + Note that the "openid" scope will always be requested regardless + of the value in this setting, since it is always required according + to the OIDC spec. By default, when this field is not set, the + Supervisor will request the following scopes: "openid", "offline_access", + "email", and "profile". See https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + for a description of the "profile" and "email" scopes. See https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + for a description of the "offline_access" scope. This default + value may change in future versions of Pinniped as the standard + evolves, or as common patterns used by providers who implement + the standard in the ecosystem evolve. By setting this list to + anything other than an empty list, you are overriding the default + value, so you may wish to include some of "offline_access", + "email", and "profile" in your override list. If you do not + want any of these scopes to be requested, you may set this list + to contain only "openid". Some OIDC providers may also require + a scope to get access to the user''s group membership, in which + case you may wish to include it in this list. Sometimes the + scope to request the user''s group membership is called "groups", + but unfortunately this is not specified in the OIDC standard. + Generally speaking, you should include any scopes required to + cause the appropriate claims to be the returned by your OIDC + provider in the ID token or userinfo endpoint results for those + claims which you would like to use in the oidcClaims settings + to determine the usernames and group memberships of your Kubernetes + users. See your OIDC provider''s documentation for more information + about what scopes are available to request claims. Additionally, + the Pinniped Supervisor requires that your OIDC provider returns + refresh tokens to the Supervisor from these authorization flows. + For most OIDC providers, the scope required to receive refresh + tokens will be "offline_access". See the documentation of your + OIDC provider''s authorization and token endpoints for its requirements + for what to include in the request in order to receive a refresh + token in the response, if anything. Note that it may be safe + to send "offline_access" even to providers which do not require + it, since the provider may ignore scopes that it does not understand + or require (see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + In the unusual case that you must avoid sending the "offline_access" + scope, then you must override the default value of this setting. + This is required if your OIDC provider will reject the request + when it includes "offline_access" (e.g. GitLab''s OIDC provider).' + items: + type: string + type: array + allowPasswordGrant: + description: allowPasswordGrant, when true, will allow the use + of OAuth 2.0's Resource Owner Password Credentials Grant (see + https://datatracker.ietf.org/doc/html/rfc6749#section-4.3) to + authenticate to the OIDC provider using a username and password + without a web browser, in addition to the usual browser-based + OIDC Authorization Code Flow. The Resource Owner Password Credentials + Grant is not officially part of the OIDC specification, so it + may not be supported by your OIDC provider. If your OIDC provider + supports returning ID tokens from a Resource Owner Password + Credentials Grant token request, then you can choose to set + this field to true. This will allow end users to choose to present + their username and password to the kubectl CLI (using the Pinniped + plugin) to authenticate to the cluster, without using a web + browser to log in as is customary in OIDC Authorization Code + Flow. This may be convenient for users, especially for identities + from your OIDC provider which are not intended to represent + a human actor, such as service accounts performing actions in + a CI/CD environment. Even if your OIDC provider supports it, + you may wish to disable this behavior by setting this field + to false when you prefer to only allow users of this OIDCIdentityProvider + to log in via the browser-based OIDC Authorization Code Flow. + Using the Resource Owner Password Credentials Grant means that + the Pinniped CLI and Pinniped Supervisor will directly handle + your end users' passwords (similar to LDAPIdentityProvider), + and you will not be able to require multi-factor authentication + or use the other web-based login features of your OIDC provider + during Resource Owner Password Credentials Grant logins. allowPasswordGrant + defaults to false. + type: boolean + type: object + claims: + description: Claims provides the names of token claims that will be + used when inspecting an identity from this OIDC identity provider. + properties: + groups: + description: Groups provides the name of the ID token claim or + userinfo endpoint response claim that will be used to ascertain + the groups to which an identity belongs. By default, the identities + will not include any group memberships when this setting is + not configured. + type: string + username: + description: Username provides the name of the ID token claim + or userinfo endpoint response claim that will be used to ascertain + an identity's username. When not set, the username will be an + automatically constructed unique string which will include the + issuer URL of your OIDC provider along with the value of the + "sub" (subject) claim from the ID token. + type: string + type: object + client: + description: OIDCClient contains OIDC client information to be used + used with this OIDC identity provider. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the clientID and clientSecret for + an OIDC client. If only the SecretName is specified in an OIDCClient + struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" + with keys "clientID" and "clientSecret". + type: string + required: + - secretName + type: object + issuer: + description: Issuer is the issuer URL of this OIDC identity provider, + i.e., where to fetch /.well-known/openid-configuration. + minLength: 1 + pattern: ^https:// + type: string + tls: + description: TLS configuration for discovery/JWKS requests to the + issuer. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. + type: string + type: object + required: + - client + - issuer + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/latest/client/concierge/clientset/versioned/clientset.go b/generated/latest/client/concierge/clientset/versioned/clientset.go index c2744edf..9976f6b1 100644 --- a/generated/latest/client/concierge/clientset/versioned/clientset.go +++ b/generated/latest/client/concierge/clientset/versioned/clientset.go @@ -72,6 +72,10 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { diff --git a/generated/latest/client/supervisor/clientset/versioned/clientset.go b/generated/latest/client/supervisor/clientset/versioned/clientset.go index 7e617419..206751d2 100644 --- a/generated/latest/client/supervisor/clientset/versioned/clientset.go +++ b/generated/latest/client/supervisor/clientset/versioned/clientset.go @@ -56,6 +56,10 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { diff --git a/go.mod b/go.mod index 3608bfcf..fcbc31f6 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,6 @@ require ( github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/coreos/go-oidc v2.2.1+incompatible // indirect @@ -121,7 +120,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/googleapis/gnostic v0.5.5 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/go.sum b/go.sum index 75df4088..224a8ca6 100644 --- a/go.sum +++ b/go.sum @@ -58,7 +58,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 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-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -114,6 +113,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -132,9 +132,6 @@ 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= @@ -184,8 +181,6 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-oidc/v3 v3.1.0 h1:6avEvcdvTa1qYsOZ6I5PRkSYHzpTNWgKYmaJfaYbrRw= -github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/coreos/go-oidc/v3 v3.2.0 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc= github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -261,8 +256,6 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= -github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -273,7 +266,6 @@ github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9 github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.3/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= @@ -688,7 +680,6 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= @@ -745,9 +736,6 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -917,7 +905,6 @@ github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -995,7 +982,6 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR 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= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -1096,7 +1082,6 @@ github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUr 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/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -1137,7 +1122,6 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE= github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= @@ -1243,7 +1227,6 @@ 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.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -1259,7 +1242,6 @@ 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/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1278,12 +1260,8 @@ github.com/stretchr/testify v1.7.1/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.11.2 h1:PpaPWhNlMVjkAKaOj0bbPv6KCVnrm8jbVwG7OtSdAqw= -github.com/tdewolff/minify/v2 v2.11.2/go.mod h1:NxozhBtgUVypPLzQdV96wkIu9J9vAiVmBcKhfC2zMfg= github.com/tdewolff/minify/v2 v2.11.9 h1:1q5728c0QICKlp2X1n7OiaiiFFzCzsq7uxAkv+eykT8= github.com/tdewolff/minify/v2 v2.11.9/go.mod h1:XHKhaRF/vTa3EP4JX8oZ2CO4crGEtVOiSoqUED953wM= -github.com/tdewolff/parse/v2 v2.5.29 h1:Uf0OtZL9YaUXTuHEOitdo9lD90P0XTwCjZi+KbGChuM= -github.com/tdewolff/parse/v2 v2.5.29/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/parse/v2 v2.5.33 h1:D75KlhAeCSQg4Na8cWKehJdPJoZxwdpRbTZw7lZFWNQ= github.com/tdewolff/parse/v2 v2.5.33/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= @@ -1324,7 +1302,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/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= @@ -1451,7 +1428,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1563,11 +1539,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1586,7 +1560,6 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ 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/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -1707,7 +1680,6 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1726,7 +1698,6 @@ golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16C golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/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-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1744,7 +1715,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1851,7 +1821,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= @@ -1890,7 +1859,6 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q 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/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= @@ -2070,7 +2038,6 @@ 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/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= @@ -2113,30 +2080,17 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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.23.6 h1:yOK34wbYECH4RsJbQ9sfkFK3O7f/DUHRlzFehkqZyVw= -k8s.io/api v0.23.6/go.mod h1:1kFaYxGCFHYp3qd6a85DAj/yW8aVD6XLZMqJclkoi9g= k8s.io/api v0.24.1 h1:BjCMRDcyEYz03joa3K1+rbshwh1Ay6oB53+iUx2H8UY= k8s.io/api v0.24.1/go.mod h1:JhoOvNiLXKTPQ60zh2g0ewpA+bnEYf5q44Flhquh4vQ= -k8s.io/apiextensions-apiserver v0.23.6 h1:v58cQ6Z0/GK1IXYr+oW0fnYl52o9LTY0WgoWvI8uv5Q= -k8s.io/apiextensions-apiserver v0.23.6/go.mod h1:YVh17Mphv183THQJA5spNFp9XfoidFyL3WoDgZxQIZU= k8s.io/apiextensions-apiserver v0.24.1 h1:5yBh9+ueTq/kfnHQZa0MAo6uNcPrtxPMpNQgorBaKS0= k8s.io/apiextensions-apiserver v0.24.1/go.mod h1:A6MHfaLDGfjOc/We2nM7uewD5Oa/FnEbZ6cD7g2ca4Q= -k8s.io/apimachinery v0.23.6 h1:RH1UweWJkWNTlFx0D8uxOpaU1tjIOvVVWV/bu5b3/NQ= -k8s.io/apimachinery v0.23.6/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apimachinery v0.24.1 h1:ShD4aDxTQKN5zNf8K1RQ2u98ELLdIW7jEnlO9uAMX/I= k8s.io/apimachinery v0.24.1/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= -k8s.io/apiserver v0.23.6 h1:p94LiXcsSnpSDIl4cv98liBuFKcaygSCNopFNfMg/Ac= -k8s.io/apiserver v0.23.6/go.mod h1:5PU32F82tfErXPmf7FXhd/UcuLfh97tGepjKUgJ2atg= k8s.io/apiserver v0.24.1 h1:LAA5UpPOeaREEtFAQRUQOI3eE5So/j5J3zeQJjeLdz4= k8s.io/apiserver v0.24.1/go.mod h1:dQWNMx15S8NqJMp0gpYfssyvhYnkilc1LpExd/dkLh0= -k8s.io/client-go v0.23.6 h1:7h4SctDVQAQbkHQnR4Kzi7EyUyvla5G1pFWf4+Od7hQ= -k8s.io/client-go v0.23.6/go.mod h1:Umt5icFOMLV/+qbtZ3PR0D+JA6lvvb3syzodv4irpK4= k8s.io/client-go v0.24.1 h1:w1hNdI9PFrzu3OlovVeTnf4oHDt+FJLd9Ndluvnb42E= k8s.io/client-go v0.24.1/go.mod h1:f1kIDqcEYmwXS/vTbbhopMUbhKp2JhOeVTfxgaCIlF8= -k8s.io/code-generator v0.23.6/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/code-generator v0.24.1/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= -k8s.io/component-base v0.23.6 h1:8dhVZ4VrRcNdV2EGjl8tj8YOHwX6ysgCGMJ2Oyy0NW8= -k8s.io/component-base v0.23.6/go.mod h1:FGMPeMrjYu0UZBSAFcfloVDplj9IvU+uRMTOdE23Fj0= k8s.io/component-base v0.24.1 h1:APv6W/YmfOWZfo+XJ1mZwep/f7g7Tpwvdbo9CQLDuts= k8s.io/component-base v0.24.1/go.mod h1:DW5vQGYVCog8WYpNob3PMmmsY8A3L9QZNg4j/dV3s38= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -2144,20 +2098,13 @@ k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHz k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 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.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.23.6 h1:/p1FvmG3je8kSv+i6uJoK+LkViOgu1vhV+BpGgibdCk= -k8s.io/kube-aggregator v0.23.6/go.mod h1:cubFdoSJRMEN+ilg1ErhNIoplJwyYbmgn3bUlen8KjA= k8s.io/kube-aggregator v0.24.1 h1:OXnkMFY20gaVV4cwKSayOZobdETOvRhgDxCHxriBygU= k8s.io/kube-aggregator v0.24.1/go.mod h1:vZvRALCO32hrIuREhkYwLq5Crc0zh6SxzJDAKrQM1+k= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf h1:M9XBsiMslw2lb2ZzglC0TOkBPK5NQi0/noUrdnoFwUg= -k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU= k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= @@ -2171,7 +2118,6 @@ 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.30 h1:dUk62HQ3ZFhD48Qr8MIXCiKA8wInBQCtuE4QGfFW7yA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= diff --git a/hack/lib/kube-versions.txt b/hack/lib/kube-versions.txt index b8815915..18d80d89 100644 --- a/hack/lib/kube-versions.txt +++ b/hack/lib/kube-versions.txt @@ -1,6 +1,7 @@ -1.23.5 -1.22.8 -1.21.11 +1.24.1 +1.23.7 +1.22.10 +1.21.13 1.20.15 1.19.16 1.18.18 From 2c048bcb4fb7814cfc772bc575967c60efa2d253 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 7 Jun 2022 08:49:32 -0700 Subject: [PATCH 69/77] Bump all deps to latest Signed-off-by: Monis Khan --- go.mod | 50 +++++++++++++++++----------------- go.sum | 86 ++++++++++++++++++++++++++++++++++++---------------------- 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index fcbc31f6..18cee512 100644 --- a/go.mod +++ b/go.mod @@ -62,15 +62,15 @@ require ( github.com/sclevine/spec v1.4.0 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.7.2 github.com/tdewolff/minify/v2 v2.11.9 go.uber.org/atomic v1.9.0 go.uber.org/zap v1.21.0 - golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 - golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e + golang.org/x/net v0.0.0-20220607020251-c690dde0001d + golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 golang.org/x/text v0.3.7 gopkg.in/square/go-jose.v2 v2.6.0 k8s.io/api v0.24.1 @@ -79,7 +79,7 @@ require ( k8s.io/apiserver v0.24.1 k8s.io/client-go v0.24.1 k8s.io/component-base v0.24.1 - k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 + k8s.io/gengo v0.0.0-20220307231824-4627b89bbf1b k8s.io/klog/v2 v2.60.1 k8s.io/kube-aggregator v0.24.1 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 @@ -90,7 +90,7 @@ require ( cloud.google.com/go/compute v1.6.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.27 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect @@ -119,11 +119,11 @@ require ( github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/gnostic v0.6.9 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d // indirect @@ -140,20 +140,20 @@ require ( github.com/ory/go-acc v0.2.8 // indirect github.com/ory/go-convenience v0.1.0 // indirect github.com/ory/viper v1.7.5 // indirect - github.com/ory/x v0.0.380 // indirect + github.com/ory/x v0.0.409 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.34.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/subosito/gotenv v1.2.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect github.com/tdewolff/parse/v2 v2.5.33 // indirect go.etcd.io/etcd/api/v3 v3.5.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect @@ -161,31 +161,31 @@ require ( go.opentelemetry.io/contrib v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect - go.opentelemetry.io/otel v1.6.3 // indirect + go.opentelemetry.io/otel v1.7.0 // indirect go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect go.opentelemetry.io/otel/metric v0.27.0 // indirect - go.opentelemetry.io/otel/sdk v1.6.3 // indirect + go.opentelemetry.io/otel/sdk v1.7.0 // indirect go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect - go.opentelemetry.io/otel/trace v1.6.3 // indirect + go.opentelemetry.io/otel/trace v1.7.0 // indirect go.opentelemetry.io/proto/otlp v0.15.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect + golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46 // indirect - google.golang.org/grpc v1.46.0 // indirect + google.golang.org/genproto v0.0.0-20220607140733-d738665f6195 // indirect + google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect - sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.31 // indirect + sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect ) diff --git a/go.sum b/go.sum index 224a8ca6..c04c8aa9 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,9 @@ github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgq github.com/Azure/go-autorest/autorest v0.11.27 h1:F3R3q42aWytozkV8ihzcgMO4OA4cuqr3bNlsEuF6//A= github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.20 h1:gJ3E98kMpFB1MFqQCvA1yFab8vthOeD4VlFRQULxahg= +github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= @@ -136,6 +137,7 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -258,12 +260,14 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= @@ -682,8 +686,9 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.10.1/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= -github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -791,8 +796,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO 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/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8/go.mod h1:KrtyD5PFj++GKkFS/7/RRrfnRhAMGQwy75GLCHWrCNs= @@ -887,6 +892,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= @@ -1070,8 +1076,8 @@ github.com/ory/x v0.0.93/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g= github.com/ory/x v0.0.110/go.mod h1:DJfkE3GdakhshNhw4zlKoRaL/ozg/lcTahA9OCih2BE= github.com/ory/x v0.0.127/go.mod h1:FwUujfFuCj5d+xgLn4fGMYPnzriR5bdAIulFXMtnK0M= github.com/ory/x v0.0.214/go.mod h1:aRl57gzyD4GF0HQCekovXhv0xTZgAgiht3o8eVhsm9Q= -github.com/ory/x v0.0.380 h1:A7QYsVQQQ0CgW9Do0+Z8QkeFNaKgXsfQ/MChQm00s9U= -github.com/ory/x v0.0.380/go.mod h1:JHPSavhYHgzlh9teE1vGY+1tecUo2CzfLqHex42jNSQ= +github.com/ory/x v0.0.409 h1:DkPNOfV8+rFrudbRxX6V6xRlfakrYpCFJesAzagVjWM= +github.com/ory/x v0.0.409/go.mod h1:Rchv+ANloKAhmN3LZ5KUIAU2TIRlHPF7EYEB2i3xL0Q= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -1110,8 +1116,9 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1147,6 +1154,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= @@ -1218,8 +1226,8 @@ github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgK 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= github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -1255,11 +1263,12 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 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/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= github.com/tdewolff/minify/v2 v2.11.9 h1:1q5728c0QICKlp2X1n7OiaiiFFzCzsq7uxAkv+eykT8= github.com/tdewolff/minify/v2 v2.11.9/go.mod h1:XHKhaRF/vTa3EP4JX8oZ2CO4crGEtVOiSoqUED953wM= github.com/tdewolff/parse/v2 v2.5.33 h1:D75KlhAeCSQg4Na8cWKehJdPJoZxwdpRbTZw7lZFWNQ= @@ -1293,6 +1302,8 @@ github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKn github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= 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= @@ -1433,8 +1444,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1539,6 +1550,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1546,8 +1558,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1567,8 +1579,9 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw= +golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 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= @@ -1580,8 +1593,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ 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/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1694,13 +1708,13 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1830,8 +1844,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= @@ -1963,8 +1978,8 @@ google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46 h1:G1IeWbjrqEq9ChWxEuRPJu6laA67+XgTFHVSAvepr38= -google.golang.org/genproto v0.0.0-20220426171045-31bebdecfb46/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220607140733-d738665f6195 h1:dp5xvm3zUH+xcW+Pv1o+1phiOKLVAUA4Y2zSmvDRiRA= +google.golang.org/genproto v0.0.0-20220607140733-d738665f6195/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1997,8 +2012,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -2038,8 +2053,8 @@ 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/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/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= @@ -2067,8 +2082,10 @@ 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/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= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= @@ -2094,8 +2111,9 @@ k8s.io/code-generator v0.24.1/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI k8s.io/component-base v0.24.1 h1:APv6W/YmfOWZfo+XJ1mZwep/f7g7Tpwvdbo9CQLDuts= k8s.io/component-base v0.24.1/go.mod h1:DW5vQGYVCog8WYpNob3PMmmsY8A3L9QZNg4j/dV3s38= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 h1:TT1WdmqqXareKxZ/oNXEUSwKlLiHzPMyB0t8BaFeBYI= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20220307231824-4627b89bbf1b h1:vEhKDJESYfeRiaBNmRvO+/12RAo1cFeu6vGm1fBFY34= +k8s.io/gengo v0.0.0-20220307231824-4627b89bbf1b/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 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.60.1 h1:VW25q3bZx9uE3vvdL6M8ezOX79vA2Aq1nEWLqNQclHc= @@ -2116,10 +2134,12 @@ 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.30 h1:dUk62HQ3ZFhD48Qr8MIXCiKA8wInBQCtuE4QGfFW7yA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.31 h1:AkDu3cwh4DPrjq2OV8xXjil+V5wsZoxUmj07OLw+/Yw= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.31/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 h1:2sgAQQcY0dEW2SsQwTXhQV4vO6+rSslYx8K3XmM5hqQ= +sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= From dd61ada540265a797726362256c79297d8cbe9c3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 8 Jun 2022 10:22:15 -0700 Subject: [PATCH 70/77] Allow new warning messages about GCP plugin in TestGetPinnipedCategory --- test/integration/category_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/integration/category_test.go b/test/integration/category_test.go index afe75b4e..d9f1db6f 100644 --- a/test/integration/category_test.go +++ b/test/integration/category_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -32,17 +32,18 @@ func runTestKubectlCommand(t *testing.T, args ...string) (string, string) { func requireCleanKubectlStderr(t *testing.T, stderr string) { // Every line must be empty or contain a known, innocuous warning. for _, line := range strings.Split(stderr, "\n") { - if strings.TrimSpace(line) == "" { - continue + switch { + case strings.TrimSpace(line) == "", + strings.Contains(line, "Throttling request took"), + strings.Contains(line, "due to client-side throttling, not priority and fairness"), + strings.Contains(line, "the gcp auth plugin is deprecated in v1.22+, unavailable in v1.25+; use gcloud instead"), + strings.Contains(line, "To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke"): + // ignore these allowed stderr lines + default: + // anything else is a failure + require.Failf(t, "unexpected kubectl stderr", "kubectl produced unexpected stderr:\n%s\n\n", stderr) + return } - if strings.Contains(line, "Throttling request took") { - continue - } - if strings.Contains(line, "due to client-side throttling, not priority and fairness") { - continue - } - require.Failf(t, "unexpected kubectl stderr", "kubectl produced unexpected stderr:\n%s\n\n", stderr) - return } } From ec533cd781d390130887c06aaa44cb2e44f76b57 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 8 Jun 2022 12:57:00 -0700 Subject: [PATCH 71/77] Skip some recently added integration tests when LDAP is unavailable Also refactor to use shared test helper for skipping LDAP and AD tests. --- test/integration/e2e_test.go | 44 +++++++------------- test/integration/supervisor_login_test.go | 9 +--- test/integration/supervisor_warnings_test.go | 12 ++---- test/testlib/skip.go | 23 +++++++++- 4 files changed, 41 insertions(+), 47 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 1c07b48e..9bafffde 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -634,15 +634,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands // by interacting with the CLI's username and password prompts. t.Run("with Supervisor LDAP upstream IDP using username and password prompts", func(t *testing.T) { + testlib.SkipTestWhenLDAPIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs @@ -696,15 +694,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands // by passing username and password via environment variables, thus avoiding the CLI's username and password prompts. t.Run("with Supervisor LDAP upstream IDP using PINNIPED_USERNAME and PINNIPED_PASSWORD env vars", func(t *testing.T) { + testlib.SkipTestWhenLDAPIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs @@ -770,18 +766,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands // by interacting with the CLI's username and password prompts. t.Run("with Supervisor ActiveDirectory upstream IDP using username and password prompts", func(t *testing.T) { + testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("Active Directory integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames @@ -835,19 +826,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands // by passing username and password via environment variables, thus avoiding the CLI's username and password prompts. t.Run("with Supervisor ActiveDirectory upstream IDP using PINNIPED_USERNAME and PINNIPED_PASSWORD env vars", func(t *testing.T) { + testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) tempDir := testutil.TempDir(t) // per-test tmp dir to avoid sharing files between tests - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("ActiveDirectory integration test requires connectivity to an LDAP server") - } - - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames @@ -912,6 +897,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. t.Run("with Supervisor LDAP upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) { + testlib.SkipTestWhenLDAPIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) @@ -966,6 +953,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow. t.Run("with Supervisor Active Directory upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) { + testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) @@ -974,13 +963,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. page := browsertest.Open(t) - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("Active Directory integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } - expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames @@ -1027,6 +1009,8 @@ func TestE2EFullIntegration_Browser(t *testing.T) { // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow. t.Run("with Supervisor LDAP upstream IDP and browser flow selected by env var override with with form_post automatic authcode delivery to CLI", func(t *testing.T) { + testlib.SkipTestWhenLDAPIsUnavailable(t, env) + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 3c2f045a..47587db7 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -48,17 +48,12 @@ func TestSupervisorLogin_Browser(t *testing.T) { skipLDAPTests := func(t *testing.T) { t.Helper() - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } + testlib.SkipTestWhenLDAPIsUnavailable(t, env) } skipActiveDirectoryTests := func(t *testing.T) { t.Helper() - skipLDAPTests(t) - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } + testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) } basicOIDCIdentityProviderSpec := func() idpv1alpha1.OIDCIdentityProviderSpec { diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index f4ae43ef..74b5aab0 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -103,9 +103,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { ) t.Run("LDAP group refresh flow", func(t *testing.T) { - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } + testlib.SkipTestWhenLDAPIsUnavailable(t, env) expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue @@ -242,13 +240,9 @@ func TestSupervisorWarnings_Browser(t *testing.T) { t.Logf("second kubectl command took %s", time.Since(startTime2).String()) }) + t.Run("Active Directory group refresh flow", func(t *testing.T) { - if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { - t.Skip("LDAP integration test requires connectivity to an LDAP server") - } - if env.SupervisorUpstreamActiveDirectory.Host == "" { - t.Skip("Active Directory hostname not specified") - } + testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) expectedUsername, password := testlib.CreateFreshADTestUser(t, env) t.Cleanup(func() { diff --git a/test/testlib/skip.go b/test/testlib/skip.go index 6f7de643..8a382b18 100644 --- a/test/testlib/skip.go +++ b/test/testlib/skip.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testlib @@ -8,7 +8,28 @@ import "testing" // skipUnlessIntegration skips the current test if `-short` has been passed to `go test`. func skipUnlessIntegration(t *testing.T) { t.Helper() + if testing.Short() { t.Skip("skipping integration test because of '-short' flag") } } + +func SkipTestWhenLDAPIsUnavailable(t *testing.T, env *TestEnv) { + t.Helper() + + if len(env.ToolsNamespace) == 0 && !env.HasCapability(CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } +} + +func SkipTestWhenActiveDirectoryIsUnavailable(t *testing.T, env *TestEnv) { + t.Helper() + + if !env.HasCapability(CanReachInternetLDAPPorts) { + t.Skip("Active Directory integration test requires network connectivity to an AD server") + } + + if IntegrationEnv(t).SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } +} From 3ebf5ad4c38cc8043a457aef4786c0b5dc361c05 Mon Sep 17 00:00:00 2001 From: Pinny Date: Wed, 8 Jun 2022 22:13:13 +0000 Subject: [PATCH 72/77] Updated versions in docs for v0.18.0 release --- site/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/config.yaml b/site/config.yaml index ce02f523..449d2059 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -7,8 +7,8 @@ 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.17.0 - latest_codegen_version: 1.23 + latest_version: v0.18.0 + latest_codegen_version: 1.24 pygmentsCodefences: true pygmentsStyle: "pygments" markup: From 221f1747680f3b326a2ee10c6a5e1cfa51a3d07b Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 8 Jun 2022 15:14:02 -0700 Subject: [PATCH 73/77] Update v0.18.0 blog post date --- site/content/posts/2022-06-01-json-logging-ldap-ui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/posts/2022-06-01-json-logging-ldap-ui.md b/site/content/posts/2022-06-01-json-logging-ldap-ui.md index 02e0c07b..6f95aac6 100644 --- a/site/content/posts/2022-06-01-json-logging-ldap-ui.md +++ b/site/content/posts/2022-06-01-json-logging-ldap-ui.md @@ -1,7 +1,7 @@ --- title: "Pinniped v0.18.0: With User-Friendly features such as JSON formatted logs, LDAP/ActiveDirectory UI Support" slug: formatted-logs-ui-based-ldap-logins -date: 2022-06-01 +date: 2022-06-08 author: Anjali Telang image: https://images.unsplash.com/photo-1587738972117-c12f8389f1d4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1752&q=80 excerpt: "With v0.18.0 you get cool new features like nicely formatted JSON logs, UI to login to your LDAP or ActiveDirectory Identity Provider, and more" From b3ad29fe1ce38cbe3a63d6f1527dcd7a489572f1 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 10 Jun 2022 12:26:40 -0700 Subject: [PATCH 74/77] Always attempt to docker pull before codegen Signed-off-by: Margo Crawford --- hack/lib/update-codegen.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/hack/lib/update-codegen.sh b/hack/lib/update-codegen.sh index c1480011..59a0887b 100755 --- a/hack/lib/update-codegen.sh +++ b/hack/lib/update-codegen.sh @@ -20,6 +20,7 @@ if [[ -z "${CONTAINED:-}" ]]; then echo "generating code for ${kubeVersion} using ${CODEGEN_IMAGE}..." docker run --rm \ + --pull always \ --env CONTAINED=1 \ --env CODEGEN_LOG_LEVEL="$debug_level" \ --volume "${ROOT}:/work" \ From 3cf3b28c5b0a24b5f9adf08fb40cb0716e42034e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 22 Jun 2022 15:12:28 -0700 Subject: [PATCH 75/77] Update audit log proposal --- proposals/1141_audit-logging/README.md | 41 +++++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/proposals/1141_audit-logging/README.md b/proposals/1141_audit-logging/README.md index 4ebe9cc9..994b996c 100644 --- a/proposals/1141_audit-logging/README.md +++ b/proposals/1141_audit-logging/README.md @@ -77,8 +77,7 @@ There will be very few user-facing configuration options for audit logging in th found to be needed, more configuration could be added in future versions. This proposal recommends adding a single on/off install-time configuration option for disabling audit logs. By default, -audit logs will be enabled. An admin user who is concerned about logging identities, for example because usernames may -be considered PII, may disable audit logging. +audit logs will be disabled. Usernames may be considered PII, so disabled by default avoids potentially logging PII. Like other install-time configuration options, this option would appear in the values.yaml file of the Supervisor and Concierge deployment directories. The selected value would be rendered into the "static" ConfigMap, and read by the @@ -106,20 +105,23 @@ The Supervisor's audit logs would include events such as: - Downstream login (started, succeeded, failed) - Downstream token exchange (succeeded, failed) - Session expired -- Maybe: The equivalent of access log events for all Supervisor endpoints, since there is no other component providing +- The equivalent of access log events for all Supervisor endpoints, since there is no other component providing access logs. This would include logging things like calls to the Supervisor's OIDC well-known discovery endpoint. These logs could help an investigator determine more about the usage pattern of a suspicious client. -- Maybe: Newly authenticated user is associated with “admin” RBAC. Note that the Supervisor is not directly aware of - RBAC, so determining this would require otherwise unnecessary calls to the Kubernetes API server, which would degrade - the performance of the Supervisor. It's also not clear what would constitute "admin" level access, since RBAC is - configurable at a very fine-grained level. On the other hand, the Supervisor is directly aware of the user's group - memberships, which could be logged. +- The identity (username, group memberships) of newly authenticated users +- Newly authenticated user is associated with “admin-like” RBAC. Any user that is allowed to perform + `verbs=* groups=* resources=*` according to a subject access review API call shall be considered "admin-like". + This would only indicate that the user has "admin-like" permissions on the Supervisor cluster itself, not on other + workload clusters, since the Supervisor is not aware of the RBAC settings on the workload clusters. The Concierge's audit logs would include events such as: -- Token credential request (succeeded, failed, maybe maps to admin RBAC). While already captured by the API server audit +- Token credential request (succeeded, failed, maps to admin RBAC). While already captured by the API server audit logs, those should likely be set to metadata. Duplicating the event allows for more controlled capture & management of data. + - Similar to the Supervisor, the TCR endpoint could log when an authenticated user is associated with “admin-like” + RBAC. Any user that is allowed to perform `verbs=* groups=* resources=*` according to a subject access review API + call shall be considered "admin-like". - WhoAmI Request. While already captured by the API server audit logs, duplicating the event allows for more controlled capture & management of data. @@ -140,7 +142,8 @@ might lose a few lines of logs. A normal pod shutdown should be able to flush th will be added to both the Concierge and Supervisor apps Deployments' Pods. These containers will tail those audit logs to stdout, thus effectively moving those log lines from files on the Pod to Kubernetes container logs. Those sidecar container images can be minimal with just enough in the image to support the unix `tail` command (or similar Go binary, -such as [hpcloud/tail](https://github.com/hpcloud/tail)). +such as [hpcloud/tail](https://github.com/hpcloud/tail), although that particular example library may not be maintained +anymore). Kubernetes will take care of concerns such as log rotation for the container logs. For the files on the Pod's disk output by the Supervisor and Concierge apps, we should research whether Pinniped should have code to avoid allowing @@ -189,7 +192,7 @@ action type may be added. All keys should be included in documentation for the a Every event should include these keys: -- `time`: the timestamp of the event +- `timestamp`: the timestamp of the event - `event`: the event type, which is a brief description of what happened, with no string interpolation, so it will always be the same for a given event type (e.g. `upstream refresh succeeded`) - `v`: a number specifying the format version of the event type, starting with `1`, to give us flexibility to make @@ -198,7 +201,7 @@ Every event should include these keys: Depending on the event type, an event might include other keys, such as: -- `msg`: a freeform warning or error message meant to be read by a human (e.g. the error message that was returned by an +- `message`: a freeform warning or error message meant to be read by a human (e.g. the error message that was returned by an upstream IDP during a failed login attempt) - `requestID`: a unique ID for the request, if the event is related to an API request - `requestURI`: the path of the endpoint, if the event is related to an API request @@ -209,7 +212,8 @@ Depending on the event type, an event might include other keys, such as: there is one The names of many of these keys are purposefully similar to the names of the keys used by Kubernetes audit events to -make them feel familiar. +make them feel familiar. Also, where it makes sense, the key names should be similar to +[those used in the Pinniped Pod logs](https://github.com/vmware-tanzu/pinniped/blob/main/internal/plog/zap.go#L104-L120). The details of these additional keys will be worked out as the details of the specific events are being worked out, during implementation of this proposal. @@ -233,7 +237,7 @@ It would be desirable for a timestamp to: 2. Be easily parsable by log parsers, especially fluentbit 3. Be expressed in UTC time 4. Use at least millisecond precision -5. Use the consistent JSON key name `time` +5. Use the consistent JSON key name `timestamp` Golang's standard library's [interpretation](https://pkg.go.dev/time#pkg-constants) of RFC 3339 with nanosecond precision defines a timestamp format which meets the above goals. An example timestamp in this format, printed @@ -247,7 +251,7 @@ Given this timestamp format, the following fluentbit configuration could be used [PARSER] Name json Format json - Time_Key time + Time_Key timestamp Time_Format %Y-%m-%dT%H:%M:%S.%LZ ``` @@ -319,12 +323,13 @@ None yet. ## Open Questions -- Should we output events that can function similar to access logs for the Supervisor endoints? -- Should we try to somehow detect that a user is "root-like"? +None. ## Answered Questions -None yet. +- Should we output events that can function similar to access logs for the Supervisor endpoints? + Yes (paragraphs above updated). +- Should we try to somehow detect that a user is "root-like"? Yes (paragraphs above updated). ## Implementation Plan From 5b0c165dc8297967bd323e793acf9d086bb25c20 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 28 Jun 2022 12:44:41 -0700 Subject: [PATCH 76/77] fix usage of base64 in hack script --- hack/prepare-supervisor-on-kind.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 6a573b1c..a56e5970 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -254,6 +254,13 @@ EOF --dry-run=client --output yaml | kubectl apply -f - fi +if [[ "$OSTYPE" == "darwin"* ]]; then + certificateAuthorityData=$(cat "$root_ca_crt_path" | base64) +else + # Linux base64 requires an extra flag to keep the output on one line. + certificateAuthorityData=$(cat "$root_ca_crt_path" | base64 -w 0) +fi + # Make a JWTAuthenticator which respects JWTs from the Supervisor's issuer. # The issuer URL must be accessible from within the cluster for OIDC discovery. cat < Date: Thu, 21 Jul 2022 17:51:26 -0400 Subject: [PATCH 77/77] =?UTF-8?q?Update=20current=20maintainers=20?= =?UTF-8?q?=E2=9C=8C=EF=B8=8F=F0=9F=91=8B=F0=9F=AB=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Monis Khan --- MAINTAINERS.md | 7 +++-- .../pinniped/layouts/partials/team.html | 27 +++++++----------- .../pinniped/static/img/ben-petersen.png | Bin 0 -> 17231 bytes .../pinniped/static/img/margo-crawford.png | Bin 37975 -> 0 bytes site/themes/pinniped/static/img/mo-khan.png | Bin 20615 -> 0 bytes .../pinniped/static/img/nanci-lancaster.png | Bin 31548 -> 0 bytes .../pinniped/static/img/nigel-brown.png | Bin 0 -> 9225 bytes 7 files changed, 14 insertions(+), 20 deletions(-) create mode 100644 site/themes/pinniped/static/img/ben-petersen.png delete mode 100644 site/themes/pinniped/static/img/margo-crawford.png delete mode 100644 site/themes/pinniped/static/img/mo-khan.png delete mode 100644 site/themes/pinniped/static/img/nanci-lancaster.png create mode 100644 site/themes/pinniped/static/img/nigel-brown.png diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 1c4a9697..2852926b 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -4,21 +4,22 @@ This is the current list of maintainers for the Pinniped project. | Maintainer | GitHub ID | Affiliation | | --------------- | --------- | ----------- | -| Margo Crawford | [margocrawf](https://github.com/margocrawf) | [VMware](https://www.github.com/vmware/) | -| Mo Khan | [enj](https://github.com/enj) | [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/) | +| Ben Petersen | [benjaminapetersen](https://github.com/benjaminapetersen) | [VMware](https://www.github.com/vmware/) | ## Emeritus Maintainers * Andrew Keesler, [ankeesler](https://github.com/ankeesler) * Pablo Schuhmacher, [pabloschuhmacher](https://github.com/pabloschuhmacher) * Matt Moyer, [mattmoyer](https://github.com/mattmoyer) +* Margo Crawford, [margocrawf](https://github.com/margocrawf) +* Mo Khan, [enj](https://github.com/enj) ## Pinniped Contributors & Stakeholders | Feature Area | Lead | | ----------------------------- | :---------------------: | -| Technical Lead | Mo Khan (enj) | +| Technical Lead | Ryan Richard (cfryanr) | | Product Management | Anjali Telang (anjaltelang) | | Community Management | Nigel Brown (pnbrown) | diff --git a/site/themes/pinniped/layouts/partials/team.html b/site/themes/pinniped/layouts/partials/team.html index c8c16375..991bfc2d 100644 --- a/site/themes/pinniped/layouts/partials/team.html +++ b/site/themes/pinniped/layouts/partials/team.html @@ -3,23 +3,9 @@

The Pinniped Project Team:

-
+
-

Margo Crawford

-

Engineer

-
-
-
-
-
-

Mo Khan

-

Engineer

-
-
-
-
-
-

Nanci Lancaster

+

Nigel Brown

Community Manager

@@ -37,8 +23,15 @@

Engineer

+
+
+
+

Ben Petersen

+

Engineer

+
+

Contributing:

The Pinniped project team welcomes contributions from the community, please see the contributor’s guide for more information.

- \ No newline at end of file + diff --git a/site/themes/pinniped/static/img/ben-petersen.png b/site/themes/pinniped/static/img/ben-petersen.png new file mode 100644 index 0000000000000000000000000000000000000000..871000817e586ff7f8f37b3112e8164e10fd955f GIT binary patch literal 17231 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 iV1Iu>rV=k`PWhks6?JUAx1U7-0000KQ#4 zpQ-NZ2zgmCIA|5M%(@e^mY*01zwy#DDPsfFua^|MH3;V|9;QYt`M@&aZ@Lwv< zRy>62GV%mMc8(?lEVRtD^n|?71Ox=!j>e{(io&A*WB%`ohtS;F*`AY*&dtq@){TkQ z&e4pHfrEpCj-HW@k&)(~291-4t+RnUjja>We}nwLIKn1QMvfNt&K7pI1pmP`Ftl@V z<{>2fkE8!B|J_b!3)BDeWb5=l-TK!-y8qmvW1yv{`(NyTrriILa>_YcnEdnnAADX0 z?*G#K|H=Nx4>#R^od16s^WQ!FFX_Lo@CHijb632_+Be`I5oKwZ+o48qiDvA z!PN!De?rKotG){FaEx@!sB!v zU0YlLJ`KsR{WAW4lBN2XXlx3SUJY)<}`GGyr6SvTvp|8l!OF`U$WzdeA!XN`}A zHLly`&hYx|_jP~wIFlWKjfI5+2Ii~!=0~e;U?38-YRv(ZFuUpl?PO36Suz~>rIRR0 zMv@+_pK48-$x@mP-+JS<-!aLWDe;mjODsb=mQm#$RoV!3(N@e7rDm2*tHzonYxXOm zEv?a_M>b;pvSm<-!YRdU4B{9|H$j=HRf^i;fn4no9`P3vuz<%fdvQacWp;UJ2Q^dh8auocBX9c({6GPGhXO# zh(Z$@Q>lJSS8>kl24hAFDKs!cZwJnjVPuHGC90(dK+X1`v=^OnOJ|QBHdJ<+b zE7Yd<`+qNO8^;c$PHD0ui-|L(8zU{oINfpZNh?<;`x{RzKcqvqPe}XzlJXje_GoUi zVYMu^vna1$s$Qj{ZEXET?4V%ONEwiJKSny+IxyeTT22_ls7;O$-5YCPO-@@ilpQiu zVAX!RdRxmFnQ_tUeF_ufeLS2n%iWw9V7$h9{##F^x;61;tJOQ2`}+N@3wh)MH((gJ z4{7iC9=VmNCGXO2TA$0DK8641Cja?lgRB=k90}UMpmRJiO z%5%4CcX~*rxD~FOT;D0QU8e0awsXJJroWRK+**DqM~#?tEf^d)=lAE#6#BSEB#oGj zW0noCU9M*Ac=cUAyN;A?O&MvqdgJPs%b-z1Pjm9*-e+e+*I6e$Pa9s3CJ6Yv1=n0& z7k?Sf6Nv7v8DDau^zK{+Qh>}|Gr3w05szN74M$*k>dPtC6K7a>=~HVOR+Sl= zIs$w*=G}3(%%1oIeIKFRQ!~pkuUBR)Ra;yCq;OBtq=~^!VSni>FlL56-5oc{-vpr- zZ}j)C(6*9z<9ceg?(RTK#>XWI!-ja-Itg4eS$Npl6Y+2}5yKW7`YaoH-1f)sQ>Syk zj_6K4|FtvgEr@T_G}renT=#wE;Yrs0BfYBx6JUakal*(SKBDAXIdS7b9OWOl*D6!#A55;a{CAFsOkuXxM@s>b^ohT-!!BW9m-S zIZU6yn`2LB$qu)oao2?TuKBdaZ3&r1Z)>feL#X-1~W1u&D6QlOxiOih7falG0v zfdc0~((aW@IZ8deSVwO#Sqj0dT~BgRuf~OVh1hO)e&Q+>6NCKrhkJsQ%W;1_oZ*_z z){^|H8n4Vv4gir)#KOSnYnxsxDCp<=eaBM~1$81}Ne+H(kDp}l^O2P$>uAK~dq1<+ zO$qGO2lkqHs2(C(CNI|m4=;6iqXP*AXc7!Qh2Q~)Hz0+$xzfvOq|exL%RPMU4%ZFi z(o|%`U_@jg{Or+? zBMS8HC5KmP3TW;3ksZ%VwHIF3;qw*uOSyp$=3dKEu-SL_#2ee|of%>QDsB|vxV3=8 zK&cP!w+iS@VY)M;lx57W1gm@Q#ymDYQb%p`X#9b2t-(U$kKa#e7{8li0;%qu?H>~q`{~gNb1p<8ysF~ zRl{mk;`xjL)3Cr_QcR4j#GXG$V>9Hc}H( z_0p8dk4J|*8StgGx?Jzga_1yv4s_b?+ABxf4^MU$`ZD_5y#cZ3l9E>2P3u#+M%Qn7 zh5>GW*S=&*Sem$vGu zs-z>tIHFx**pi~xKx00FM}**=x83gnaXNQ;@AAS!iEYC zZ3LXm_+XQ`s+q!53}DKqUJSa?-;d4zhWn{=iiu+UH}3QH)!0;8E8q9dxO#=& zlG@0G%Q)`4Q9k|4hhx6e%hIbqVeq()6%NhRWi^(biKwrX`73_{N$Qi6VH%7_BQlN` zm7M>ej2#`a0~j%kJ!LQOx&{-GdeI9&ueJ5u@uCHm?vGN{NLEghV@pWe?{|K4)H!j* z=Ju^kbQKN6NJR%WH?wrvt5{lE6_-@xtWYL2^kdz6K^K*kNova?)FvQP zf&b_n2}jB^K&*G#yx|UmQ?7PtwB_Ff&yc-vEk#N3AV!gt;Lb|?Wv82B)^p!6IbAcB zo`mipn*@s>Gj@3XxmVwDxL0gz#hleJEi*$pu!|Z-k@;~3Blmfl#GS7ZQwg{rV>#jT z{g7JF#H{SIN~Rsk3%fGg0q^>eyC`klJ*m-B>+2nE-yDCg z6D~O2H>5;ItREqgiH0YE?xc{UNbjgvllHrev{Z|R>p{^@938o0V&0C9E|wi6sxeuZ zv~57jRflUU{n^_iyrv_9ATXD~{!>hVT>=WQ%kL>6mHTCiY>}R$neq_`nx3A0c`cmyf7jY8O(UJFd5k0PHWqFae?RYs(x+1|Z!}(Z_ z?r8xB!~K40@cLs{to0amsrPiUo)r>Wr+(}f!iy1;>G=2%tDOoOR7Fl@U~dY8jw-*| zW3|<*lKW-Hrbg<{nl3>OyQrf|TbuTWZcS0Jh^lI; zNrX2BQM4!_)a?Mi7q_lN$$Uc~TT-`&wS3Bvsrrfc7~BD7On5-6a&1jT;lWty z+?)WB;z^0(bVlfp!^G?rCnTtFoB;pe0fTC{;wo;nf{6GR9NyK&!kDQf;H$nm7OV0^ z|6ODib@k}OWa5ht9tDNH)bTQWc?C65TxlavUsx{5!qb!2)=9mh8h)J)kN2yG5{hOde}~=H0E{dtdj6p3iv|1vcG=lev=jz!7CBnVj_P3v_3T zHYFvDv4@F5fq!1{4t1!ExQGis-e+HTgOyvRg0J`WDt`C~x<5>)OJw!faE4?#HC4ir zvvf;y)7WxJj3o7rM2Mn>u0XbBiaD{RbxMAKK#oxivMG`f98yYuV+M+9VuiG9 z?#QY0tD&)fL4QF%lO5jYTX!6{BXRx|4BZzX1NiMT+Ccf7N!^AH+?gSu?hqI~Y zS0)S-Vc~$3T9m1Ivx`etpkwhFxjJS20 z^e$>qo!{DV62Z>$ql99Fm7+f%_vS$SXlrv52oeYc&@EYTk|&44!L@Y>1B8Z0#8%lM zta1G3)*|P^e@;UTg_2R-+#kPqP-CMMakCi_u7nZr37du^M>fHDQ1dIk*}{+NJYS|{ zoJp3~j^uq%(U=4Ygd59X)~rsDy4>qZ?~w`~9v*QRjV%R_m^J+P_(C6f+j`MgS*YGV zyFg)S5ODjlqWMU2uRFaR*=DF26+^J*xS3+A-3HDj{2m_yepdg|VVvh8v#O*HT%k!y zN@|dPS?jJ<^}STvCR<-)t0w>sP6886p6@p>>Xw$Htu|X3DPg#Ta;sOQf@;Ep_fkkH^p6Ks@zp*DY67-0MJ~(K z0Yu~lubr==6#Lb~6nf74hgeL>&`=a#!mqDNIM^lA_XDzAaZhTKxSs3Xu$~JOwWEVh zCE8R3`eal&_y;@~m%F7#WYd*xpA{O%&taC=!I7)fO)rG8p>re0=2E!^iXXcU8i9bk zEbZC{H@I<_nI6iO8rQM@kAj0Ew8YY1!TpZ;@jwj!v9VAI5a$i*I>H-{aEJ~r(bpn8 zO);5{{1!C(UrFWV6azs`#1oYBq2LY#;B^LRvz8c%TDc>VpIw7QjTn@mJ7-xSPDmy5&w_Nc9F*V?56Iz$)4wkjt#&_3Sq!eiK zLN?yi^xP#oync+yK{#HVVCD={CW{<8KFm9eIlP!SNp(KJ%1M>;QL!AFINHCP@?&Nm z-a%+U(@aQG5B94@O`QEUwsKBGrV^cBTOR)OCb$~f>C*0X*8vr27tL7?nItmi8mo@n zGP*@90>4nDjDAi;c=ZX-slz2SGEG=f|0w}^a5*!>Rv_X}t{*Zxi&nHGDLQ@&vQeO@ zt!{nbP?; z)=hOMH;{~sjM4N=a1_}s`PEM-EV~RDm~2< z-oo!I><(!%Yfi5p+$^uYabP{wnJTq7{jd42PMIU9S)+gBG$9d#6j^!GR1< z_UX$v6ndHPyy#=7p__$9f_wfE?>OyJFm`&?O-r`LZ-8NzGZ34o$@OFXq zxD9Z;1JLfFkx_7XH`j@mFgqC;TvBj%n7IucVDW;D^{~f4WM!nK<(K?Tq7-?<#=;`x zt7oEsyf-z)q>QH8cB0Z{k6CEt>m)B(UN8VZKg};)wapj$%znGW4j$&IkoC?lVOVAE zsj>&u9U3YYt*+c%w06NHki!09b<0x!4+lV!xyNo7e~OAz<5EM+5}KHnQSl6LTMc%Ild zfxqUL351Be5iUJgbu>=T?t*<)&{Lv1L>*%6Z6nlSvyBXLG>i4t$Wk*WIOeDhDCdwF zYpEM+5%a1E?-w+F_TaW}Z4*X#SwZ` z2VG*!KgiZ7tIDiWAC{}EC-2-&j4R1aOMahVn8;~jVObF6^pe`h1GUz$|3Ffl>677g zi8~EYqZZ+)MztAg`KqFr%tu5@NF&st@Cj(;qW~(O#R$wk5Ux+dhpEh z$6vGb(B=-Up>(bcm)N@)uSR+CyDzSlG9d9;lvzTS%PeXm4#Tqqr=+9Boeisn?}9=? zO89)#V#v~YvR$J!>zu)WotvL;NZ|Vnz`?}F4M~@j3e?o-6lnBE|ItCI)0mu`2YlsG z4E;g5&mv`RGBh~+PH(X2eOD5PaCy&JW>wh5ri>*Dzudtk!P~M1@U~7=2bCjR?Bt)t zl3ha7%~eA~GbOoH9+1Dh%j)p3_OIOPk)WWMO1;V?ay;E*;c#C!vMzb_ZDD1ntc)V= zEraZ=S*WgnbY$la4GSZhK;*QR&&8jI6x$8`9U~L@CtEmEJ$9w>bI@wrCrj@WT5=kH zqV0{FFD0PdCEq!YnD1fT&8LXGbm!MtvisThSHEAY3XnPd3Fj{b8{NYn(=_4?bR;@) zrm&49>>W^805jQym4d})y$azf2}N~z(v74#vVE(y?dgUhh1~ZJi^s>xm$0PF5aTQl zwOY!%v8bv+RRu#!5XADAjVO1XnMh}b6qy;Rv7rMh*jkwp(lmq+yQ!jNWyN8QP}rfS zTP?fj(V&op2`QyIebEp9Lx)45)V*}da+4qoYb=vV=zN~0VbOjsC$PqN5_%(di>!>5 z0QAaME2zSye$cA4AEs10W4}@Jb_0XQsPqRK@@oP}F8sO2)rb`1g<@)>q#{=Vhd0Z= zSrZ)BM_oLofsU8LSW`o9-GMBd>g1a4+$d#VK@+*T*>Y>RDRNy;cdc@FIlZpC{SfP{ zh%H#RxgDR4kiKuHVKo23RN=eFm0o!_gO|1a5FpK#a6;GJBI>=qn%?pv2HRmO+UU5U zIN?H_l2YRy2}*6G6mb?k;t}XBMSWs2*i!5G{7aX!%tJ&b)&hw7ox&M-2BnF!rFNv1 zqzQXF!K(J?dpe8bGEiC7+yjLP1tZc`N@_ygo-3y*JV(;JB~?7$`|D3!_ehzZm>3xW zJs~2h1Qv`+o+()-01Y}wo%tZJOJ?nEpc2Ku+>mJCIVYR^wZVH-Y8sW`&%Z=~-OB3fi5uC^Gtuux$`$c|+a!6V;1o{hDWU72j>IOFd2 zz{k&nEK{YnaKbk{Mu)W|cPDNnh8(elfqCd@0O1dQ#G`%E0)LJ%qge;OxfQoBWu=9d zd~38h|I=JFxDO|ezYh-!I;%6@M@)&%q&Z>AWpP!a$X1OE>zbUzTnIKSMd%UE8)Yy| z?jg$+7X08;Bg3HG4Pb@H3CL3A8)|O3S)4wqwy@f$_gOk`7KlLLfuxpBH}}G0=mUbw z0{2YuQYu}lo%1i*?t>|EesKSwFcnqhx;{<#a<sY5Q^EjN%_DQ+ zDlw$w3rf@C!q8za8l1sC*L}|=r%h`BS0+b8$yT)XA3_O99xmj<*za34y|?iMB0Z50 zk3sK`m)mcf(JwN%kMbG6afg{2FGarx^xUt9S1F#jDcMQls3_=5zqb)TBA4o9MO8&& zOoA`3>ucdL{iK!YeTP@wzf}qtS&EX7xzTVk;jNQYQ!Sbl%_vf_mCAc~Nx|L%P`j&e znU-+G7qP3vCNkmt7CzdbboCL!^_d{vCyiO7#d>U zU;bjC-91yfJUb%pK-SFK=LDu6)rhK1hOCf|&1m{n-cn%39CJ!k&gSCB2Lu=h{~$s9 zy|_)=0`VK~kS4(*W%g>I4YFBKIA2&1{8Ax~H3%fIOD+y&?n7NGC{~6mTpY<}(0_)S zqL)toJtEG>y}9i|$}HFWPJ$~Lj%!D3`>PdFm2toEdnFY$c7@%EX?u+7VKp=JT!v{O zs)W{kujL4Cew&Th1Qnw1vxvH;iXRJ6lZoQ=Z@Wkp0J8EZk=ljKiV@|)BrH|x7e7&t zxWpa{CXT&AkfI+ZhhI}Bgh@RG5{d|9c|!A1^jY9pWS~8gs5pES|8Efey9t^fwh5e$ z9SEux-}zc5&(k_L9j^y!j`K}F33q?P*C??xESc@xK+qYw|!5YfT z)4Oa+0{WX1pwnsru(Ux`iVcALJ?Gj9=RXR<)Nu;SB4xWe@TvI_`-!XZyy=0VOK-Tm zS-Vk9m^&nBdwL^T4=!2w|?}T*96h81V~=TgYehU1iG8W#Lt=D`>)=dD~+q4 zTFZ%<=b6dGBFR1VBC zuXozlFpTIpCTyJoeQ51}T{0er)V~Z*&rg5D?vtnj(ZI8&Xr>HF5Ab>Xrp@VJP(LXn zK>k`*Y!m!Iz)R5->`WvtmQUjS84Qk>kk?(R8+tGSps8wdFp>n}fZ|iqBiX>paP>_x z6|xLh<}Wmf-qIUr@oZ*=Bs^FWcNZD^(a#hbbR`U&8HUK;yHy~Jj-4Y&4*6ADEb%1< zJq2lx=(vKKHuu7~6Squ^{o|3YSS3CNDKRGpi6Z~-#E|K9G&*U7e0Gl!k?6%8*Dw{L z*nDmJNY~E?n-))3OX}eG$42xK%IE7bz8~5?q5D{dMDexuMkGRkZ>x<{)SMC>JBo7y0aIXq=aNls;-JsJ zX;E1m4>wC*@wBhVn(7L8ucLFf@p^1*cu?|@i~prV;j%n|kmDyKDK&n}3pw(4x5KJ* zH$RBr=%~wZb!{G$Oa{imL)X>yDn7{F$^moA6+()d7PYwkp2d$r4g4kGX(1k3QZ+ky zxhGPxF%s@2YuJl1<2@W&9gZdZh&=~_2+YGBhtH^dL-vNBwmoY&DEoNQQE!tlk}CcJ z^Nq@+f1W!o5_7S#I#``VRuE{xy0s3-^maGTN@bve8=EMC19q{8AtDY-D>P*Uq_X*z zX4JwaP>p%10OQtj)b$etfs|O~d%_dV@}q%$1X7Xi&P~r7)!YGCPhx9|aIXlq=UY}c zlXoy&-xJq%=2r0^^APM=5Mu8oK6h4n-!sEX7E@uQ*BXQnD(vSF^UidA2~ct_#hD;?VMw2yB;YP7Sp4{DU_ONzWW`h0u~41gzEk! z;7tWHU_^c2$f$}xG04%A0}2BK)58Op>YWMmlkmjc1KHZXQ1tF4wjaN{KItHBEiC4& zR@UE<)0cdtqW&l|BR6ujl|6O9-I4X)YB?I-Ay>1Xt=?or!^(|+ILk-EQ~}ALf@&&j zX_GLDhsW;Cikoed0>B5TkQlr~pegTQ!DdE*4{5aG7Q_UX1UsZJrZhgjFPrF4Dg=^_ zdIqCJ>+7uNXHu||bZouK){&tgRA~JaYtqpzU#={4mKZ8Qr*Y1I<`;*~mzDO_@lRq%hOIuZNV`)eMTrWJ zh6Jh7YPrRFHcX3$Xv9rnpB1-tg~Rm_24o3h=HR5R#P!fHj1PCepSUH}EDIK)&bD(p zVYrmFpf`KZKUWzBk*NPs#!&>vS`ezxOoJQftmQ;cAx+|P!(U64ss0QcJM((_x745E z$a4I#dFjVMzeh);(R&;8{l#UGhTA%mo|A@XwS5!b^)YZtgT*iNWRVrb%-uZ#W)y)+m@`nTFp8vn6a%Ei*$(oGKyAGp@O(wO zklacEIIv&(8Z0{OOX5~5QfwiiBAXbYwAe!TMck4?ta^7@Ln6iU#U>5(vv$nCP~3k9 zu$(zjcC!}?G^hi;=y%-x>Dop2ICh2goR+1HnXzAZm zV?NeGU%2d_$~Z*s1wWn>X`2C3ff}@IusD~(F{+ZOqO0~yjWE3m9PJ?d;biKirKKDAv1*(wHjWpR3CMOipxuFDEvwD3I%G6W5w-^ctRdOCTDQ){ zDAn$y>3`{KJi}}un_E5G6HzzY zgGCo49=zDFQ=g?-OX=>@K#MEo8I^eB#l{cCUe)-*t)fhUQ>zv`-}qg&K*1yoS1VjxUW_IMgl8?@?lFxdNO2tDR!mEQT~ zwcvwM9FM6NsLc|Y-}&-pYUl`js8PPP*Ey*cI5@YzX+;gFAc*dB$njRn)T4=FXTC;@ ztF!pgk;iE_<8LxgTS+S!ntW>sQn652Lqiu1NO5F;f#7U7&mw1NMZ#dT0qKdI3)wWKd4m!Q0tPV{SB&vgBQlaZ2&OYUmF?RVLd56%=B?ituv`#7ec!-;) z5`Z_X@lXbpq4&8A_3PSNxDn}7;LaBWvncXIB3Wrk!k0QlMCifQIv;VdiO9c?qQ~$j zBr_<5ie)~EBw+kvvBxyNRg@ee{(>vZ3aY`#<+H;>#|R8NPZk1H8OJ?oTEQXU2c&J* z_IjPg`{|eOAV|#lo`3JXezeua@h>}RA4gLxpM+j^9bHwL0V5+rtwzBruEDc$$&oJ^ zM-!K{7TubcYL5r?75_)m*-O*%5+g$s^p(XWaTIV&klQ6@U;_ZY-PeMD?{|MIOnT)} zb~#Dcrrh}y70csNXawI)y;pHXvHCch@;E@J010N7{d&}Lp8&^YSAk?@B;eq2Ka$c{nuy(NSI@`_oIscGnZ1@(?k=58N_r&mIMxD{-5x+?&iDSdak-8T6aSLmv$Uk}P@ z(~8N@B4eUWHh{ezU!9I#_parmGyr9e@?X`C56M&wHf&{f=^uk3MfMs=V+O$qny@v} zDaeR1Q!;rLDiX#5$wQ1{WfJ%I8=$q(u(6W+p;+zjTkFQo-odsg<5F(vIBUInjsjDg zYBd7hO_;A@ATi6a8BtB5iw!0*2($6vP!5#-g-W&$MKN^~K1?TV!2I#L)4c=LNBz&% zCiXatZgjd#f1ka(TVnj$>O1_Y0SfZ|&=Q&1nr3&h6O8NeVzebYXQL`9OAm{wP5>U6 z&9pEzMZ0_>%@oUlE#jLb7P2`sV+oM=C3v`{ z4d`F5&s?e`)b>=;y!B&vH+0;Gs%MTujCL{{Pn-1Fp~tM~UC=^iqQzorsN$Frc4PTT zVPYssPoibB`Mo>jRiyoDYHPB;KkJk(_an#qg=gKEmy=E{?5%icg`l?ESJt-Mq>};k zZeT_3Bs5=|Ff0dGC=N)6yS{#KmW++8WeVbBkntEYXzso!tlZ`7cqFjov$+}iOj_p9 zy*LF5^ZX~SHx7p-wtj>JVanrH&gQBK-5Y`R#E{zYc595Ew1B+G@mM;2iF1Zde>AJ}M9v^E5L{jIS!<2>!eMYrb zR12c*sCPDLEj#Ljalxv@#6^FUD9Cj1kGBWEa}#-Uoo;}muRIhd)Yptr(e(u63*NoX z@WO9P(+)*7B@JR>MOEZ(7Dd)aAbEVoto*FLe+YLfZfOxXtF$r!i&D}o*m%Lxj#HLOw`(E<@AMRhyq8pw%49=DQ` z2enKY;zltmiHxbrAPGJ*Ks*eo9Dg#8q)I;mYoLajmC34{klF}jLRyAwr}gkcftp_& zWS%1z!4mweZyypT!?Judi$rMZQrsy4)*UH!LuOm(ihV{hEpPwOhoes4-H2zmN@1RD z8ux6=U5y+T>t=WOzKUae=-TWpZ7P;0eJefCs_mj|z1o5-y-2!kFLI{_a_2;E_?@U8 z*`ywxn&qa#H4P`Gx1nT~uU@n}`x=HHdxBuJiz*PH`DEKEcTH4qck}odSRqndD8ptv>It_zT zIXft-LLe?2qnKZ(*C0QZyVi3bsK6yKnAFqPJr!3X9q0D?$|QdDl#%0KJO9ft*CA@>)l5*x=|S7-`!+(q>NOU6~C-a|7Cvc7BT6OW4X+rxIElN^GE$5)*@M zoE-YyzMI)lkM)NQ?g`?pO!MBA^@^Z3TFPz4<+R72O$m#bk-24x8qI@A$jQLhR%g`r z=>Sg1(rE>^a|kYoLGXE&=Lv$Gt0F;Gz`ew_or$2 zwX$}{)8TatJo-^nO`*;Cu6h~U?q=b&wX?&E(^g_!O@jg3B4Xh^82yEqlPLlBnO^7M zSk#pxF!Qxbj-A-YHJ&6B`s_P-xvK?8NRHM1Q1N``TN>&q{5?g;jEsziWindc{HiQ& zucP%=p9?j<=tO;170twR_nrKAjKefjGfVNvUy)!0X02Hs=M@jp+40r>)!FaobA%ds z+ec7hF#Y_H;?Byf3xw-+?YgQ^65~>23zSlaZ{oNbq2d#uER3jlAUA#3WLbB!?|8%y z=z(qN0*u1jAY;WV)-JKL2N;L$0G_h|&SaRCO-!)!i{ofL|dsRus@gC{S~O(b(+@$@o&cRTsT3eEHoNk&9-Fs~W7~ z%Qy8q+|(O^2m2J94rM=Eg5j8;7-bt)wXM6vpH(PEoaO-|n0KBoG7+!{@MF#AI*#G7 zsb_=L-S8GwmCW3V_f?dPQBn6>0t2vJGK7c(B<8?*Bn_4-LgyIX-^Tn{EiBMOeT*5T z558!F;GDTE@M{PPnM^(gkydzP#?`uU-|#8#cED9e{^eud=k~7B-tP-qlRz{tr4pF^ z;J#^54T;$_*U}u{%VVe&_WJjWh#4c|L`H%#aPydF zmrjeBU^RqI781eE9Ufg__IPv1ug}#-%S8a0m94|bU#9FnZ-5sSk%R{IDrZNpKE3Wk^ zh-Vv6h1O{yjXBv7V__M1C_i+{a9}(lwhqg zkxNl{RCO08L(Kfp0pBj^Z5kuSzrCM`yW(VusN7%7Tfru7FsclpmIS8g(u!kS9AWkeQkVeH0w zebP8!W`9#RCo9F_j{SE8uT&A|CCpXI4QZrpjF<50u1CG(j1Ge#fON$z=4H6VgnC*Uur zgf8^Z!D(tti6TdvNw4>A2;DfbfwR>+8WCRw)-7qIDATU@b@mVA93jb?9v=x;cFn`o zr)?sE670~;B*wpPcoFamAomLG=U*~=zekSSqjtSDFp9XH8t>f+qt2FEFb!a4<~uYx z*(zFTe>{qkdzFxm~S01lqah@u|cWk1)&o zA-Xz2&4q}*C|3G z44-Rw8VhJ>M@ASYs)pg%8UqmAy+Rpumb=Zqjk*mLXZ#T|>9M)j74!bUS|tE;yIkIXyFz@&sArSBPk1K5 z0-O`*m8QnsdOmqDIY%BobO9oXO9vjCi+Rwzz`ts&l5(302s3r@|Hk6Hqvu13mM&

$ImBcBlB>^dJcWRKge(Ha9#xZ{YJ=N8~d#Tm@A+;-q+UPm;3$ByAFuS}jTG~}kVqn*k zBh^V3&*!7?&T)ncC=m}%75;~c@-(k%Z14m$$}|Ci_akag{6JF0g(5091^&(oBaUTy zMBnUOfLwE(JWwT0HCY5?!7l{oSbnr*o;EpCHd--KGVKPE#oaZ zxWg}^&&}uaT^5C~aPZi6-V33u2crb0X6-1+^H+|8=?b1wpa&T!$cixWYY00X_J@fV zYc3e{$tj*CuTm3K2Uat_DomuUt@rk3AnncIR%HR?nxNJqU z%0u;HhkD>zadRP_(UJfpN6a5Kn*t1LjFrlMw_mv<&-lol3AvWGX|Q%rVaEkF=0Qt zN?Vj#m}DJo+Vs9oytV-s$_d-hhd4@so{NE=v810;I%*V|PZD^26$J&-tah8yy25qC zLuNXN@b(qS7-Jy{PWS1h5T;L0kM=i(qjq0+wNBd}{l*}ri(Te8qmJLWZc_G~q+wQE z1dtfD#uifJl&~s6M=X~u1pNDVs4sQvujHwm~N+>mfP|E|sO&l>!e`UK}jv%Z=bVPi@>H8VL z%V(YYZRLHaD8@>)7B$zY@gR$IdH&4~W+&*-G&`E!1l|s>xm|%{a2yiG_|x1}E;T<& zkH`HM+zvXHH?(IJ(XoIFGL$B8rw0v{7ubNNR)S{E4EVdUj{ZHde-IlCJ1eVYy4CW2 zig_Yn0cNOJ&$i9T#yv><^PIRnasmi(A67Y zO#kqPy9EB*otOy@C+rH>%*ez*VhXQ8 z(Vv+*a%>zPD}$m+3SL3$iY}2{EXwP8qu?;;bXa`8ophMsvEMb|W!w3qI8%oGrv#dX zBen6kv7c_YGNU}2C|z}wz5w>ruJj~)!u6sPh!$r$AA)I`0Z8f1IH(`?_>@4wAMS7s z3qE;@OsRA2?0pq*8$+C-R5kE)MDW`uOuSS%XN6Fyd;GjKE1~0%pE@y7*-&8z+?oj% zIWx)^zz}2c94-&R4}bvQXD+77HF13Ky8$GAlh#GFX2wOhkhTby~z%YeGQ>( zMPjm8+@yqhsMW4=5z28%cW%QM=F1i0z2|g9*+N1v67FzT?N;<^4}e^(lV*Gg2v4@$ zVNKb$2K+D|D zm!PEik))EFJcK51aOLsEJfqMVlr?Hws_}`l3`&p3ORL3@o2KC2#0Kw(yGKT#KFIi& zBRnyMiF4u{dW7P@p=ftQr%z^d;V3ufUB$*%4p$L${~sI@I-21Vc^N71#KOolaqH^6 zy$SMzv3lPV{@S=*TG%2RW|2zCaXKYc8rV3%nyCrxu$RB?#$tnj>dPzPuKZXWVv})} zH_o0o)MaZ%n(geE(4!;|C?T>2DHi`pA*{F}n(1rSm|!{ufKUwkSnDo>wC0Z(1sd6u zb`Ey@f_p}6&(&5drb1p4xA?C>8Rw@Bj~q6N=7&xy>TS29*JPNd3S}$hegqFve;zEDoYE8hVJ}vb@GsMaoIalig=;XIrXkX&cUHft>Ko^ ze08VFjfYQdjdLd%>Dt_;9uGosA3xG^Iy&CQVaZ0-kB-n_(|Y>bLXVn^>~K;uSM2PA zZDYIeGHzCjz~&?yQj zuPlvYH5KsvLevuwg-3+bMq-r!(C;_WQ!o&KbQdgq+7i zFbR`=DJB;~QKvZ5fnbT)TU-U0vXF`ER$%BO!}%p zlx?n^A{_4Yr($9#L8vr_MV@*m1V$d-Gef3#kaTIZpcESOI$<%taT@9pc-xiHPhiy$ zp*$FUWwoWaxpAYV{8IdHZuU2JRwmnZK~Rw(O9jfQ39!*a30vL|bK>;cDjg+L~ZI;HFg}mLKx+7g;9a$VnTBH{{u=uwZA$Yvg#k4fZbUT zU)m|Zbm$=f*s>g(zak5%F!G&iwDFLcw$-rKrln1ks4_P?fDCAf@E?%{erBeAZQs$!AX}v*)PQ(mgx61ytFwgbwEi&0f92BtlNgW|I=Nr(_YMFxc>1+o+YFu{n` zz`C`z)NsO&k335ghJN+v#Dr1)Oixd;{tnbK)o9d$I1$wWc*4!kBCw21I{-;I5s=N# zOcfp+GSjtMX4cB}y#2NS06+jqL_t*Y+l4+$UU8l$Ti4KF<{U-iEXV>pQ~SG$@V0Jg zF)s2d(+ImZH8XTp0g~jy*egUrP5`2w7Y-`7lsW%#lvoL??$*HbMJ< z8SqWeu5#Ua3^9LiNmc=4IlLb`!{iEpENlo>d*~r5<8u>oZX7K*X`p}zphqr5crSV+ z)CSJPB5TQk+YbN;!}9%Se<*Ko$`BPQ9_Ny*H;V{Q=cAF)o_$+3v{uQ+jSWV-(n&?I zeM>tzHjhJPkPwYd5*4(bw#(4)AiP2j>KBB2!$S-eOp_98AYQ_>Q)lG->3#<76erN6 zP`NGF%=us@Z;m&DDkS~;x81qd?PD>a226U;*i&Epw&e1LRWcG`XXF);*ck%#_!+>a z^jTO4N*)Qnbyp)h#)WcWl>YA`XHnTn@{DT2<%VfY=qN}z{YxDt%g-&QxlecjC9V?Y zo_zJM#7vvbO=Cfo7)oZWvO&TK)l?-|!UkI+a?I)}0AovX?$S9)5Q?dIklpL6Y+eGTlEY;{&4-CZbH;=$4-I7i3{<5?vS@i~2IN z8kPlwal?oJ#%4Kk38pG1!0`~|L}QSfRnc0T9YXihDkF&#U5p_F01Oz|z(-D3-4=m}LsU5)nOr+KIZP)G5CsmHosLL(aXDVknVf7iE&25C zLWRQ-Q#g{yTIw3>|7dM3W|>ELPVzN*sJ~*kx*p;+0ne*KF9id|m3bt29=MW_H(q>| zv#z$05A$S^`Byz6a7X(lL#@I5gmDqt)>5Kk6yS6A!bRf{GF2;{Cr32{raBthXmyRF z9>Hse*EPXj!i^FXZ@&4a%$rvYs&bNAaDI74tnA>6!%Kj^AhW>DEGlPb;9p8hQal>6 zQ-A;*cdxwm@(U0nO|pMmrxfGG`H1LCD~dSynfY6;E2pA_ni#s0aBAiC!#v0J$uXIk zUZGYk!_)H&Moz~=b)mIQXi*S@$6xHdEPbOhi2XUWj(;g?XFae@m6UEad-t_|dFHFn z%4h%jk7V0{?G#H?9G5TQQBebU}4q>-udDVi@!a^GEd;HA{`2pm+{|F_RP&mn3J zqJJo8VtPh8+FFexDL7otAX3W83#7HV*A8 z8GKZzs&?U{~ zMD*4G&Eq_d;a!lgzw{kB07%d#rj77Jpa9yJOEqJTp0rOUi)i&l^9gYf=`dLfbDSGF zIKa%zwD`>hM$U+(n8vU1qyB@}9Rs-#Hs)65(F9jR0dZgnUOYCBye62CQXi+!0-7A5 zthAb|#1H5Fp#zUXBxL2LtxK{G>9l^ay!zVf)Zr8Incx0xj!EJGu_{6x5kZA(TI#rY z4n5lNmwuBLChz&mh0Aj7^cfIrM&=RLxyYxpoCLH&idk6&QPbL6Bs?eSK&c+9;ow06 z0B3E%DZ4Q|*%)ZA8Yp-GLa`pnR8(5bF-ZJ<2GFlRptL8<7b!-^Je4QcnsnE}-LlA8 znE&sed_hVvWZIe`&o*)R>JqXiND9^@0#Q*y6v>B29~>HxjWu-`gGR*AkTlgnSJELm zv~#bK_9$TiaZs+beqwA8H@ribGIAw2BbCi%vhB83QX-B8P*W~iM}PEarn&h=HUcWl zEjzaV?&MI9rM0Ckn^#n1YN@N2LZqA8f*Kkhm5O?fF=9$RKUpe+!BI#C$aek+1)H3( zI49jV`f0V5A(J6YxUF*SMi1V}VJPJ}lE;+$8qNA_!YX^}J7twFhlPedgC-CpF1dei z2a;FnzF8zG4oG(f(`vj`K-;CAaL^ zhIo&1N$V3fLl6XS!!tK*+-OYKmlYLCg}(%>P$U6@j}xOhQE&K?OimEqeJDJ=5FHyR zSIUW0I*Y8LHk~lkMe@*-)$S0|boM7Zln{98>Lq#UrB}F42!{X$XQFZnMJWu4ai5{S z5xL`m!$c&roOt7u?73|pDwPJF%Ozvr04L)4Y1AMiOY`JUbmb5zE;#1`CN)&>&^h(UT|XB$=rWN~H>?(?)-Yx?V*Asy;0RwGwbQ3$C=uo(*Txrd4+Mz~~@=As}zOd>jH|pKLB|k&J zDO#3ipMRFeHzEycFgRb$fe};^+T+ayc<33GDUh*)Fsmh-f<^tSW_nhK7p6x~08q5= zBrP}qC-|WeX0^+tof;>h860G=R$4+Sd9gtI?$lV1Y{1(GdPa;Pc&+XE!&r>t6j0X0 zC@C9d27Q^H!BIson*D*wWd`pi8Y218+Q)jo!7B#*PFGBHOPJe0U%6i6fMJob$M798sXKUWdLGQ~lCm-ztUa5;;D58m+TKnwzTSt!rJ< zneUNh&}8RboYl?%2fuoSrQWAI;9y7M&VR4X#HdXax|4tk5{=W0DU#AurFe%a7q_Aw;iS8!Mt~JqK(*Hj zY;$S6)T2^SJ-MbKD;uv6$xi_Tc+HI)-WfYxF%+rVf7U3YG-@hMM~hR|xn2`TFHKtx z?A!g3t5>g-zVOTonYB>T>?U+hA;SK_Gsk2R5Oee9HEC~P43uO3ZZ7n2$aNjFM|${} zmp*Qg^3Ws~PrfDhG`)vY=v;DP^n#S(!8b0PMcL6WkG=on(%^4k)O1ETBpjlH+=twy zkW!|MiGFu=J=}GJoH=`5F86eaImamv9K4%yr&xabOMfXL(=t@!8vHj$C(*dD6P7pB z*W;PccBGNEhI)gjP_Uyn9|kuj1Mse_w*Hh5fznf{kJh{q0AZL9N^d|K2BdDr#aUEU zZ7}Y?<2VQce@$*R6y^pg1^@I+U6<1*FVjt9S`+DS zgbl0O^2Lc&xi)a!5FPe`b3%*>>Bx$iwQ!;fTY&~v5j%`IoskWGmgva`=uN^2uNOIQ6Jk&UKxW%fSoe!vYzk z>5yQ&MK{t2C%OjlvQf&hTLBr^=5()G#(nojePTTFhg_mXZMS*Yuc4wr=i_`OJ)*ym6B9ZUiCxl05y? zX*vJ;HR;^EflwQhH!dBA^A@m5RC<^$DMkHJLQ05(lvf!lcN3zuQc*#DKF2(yVf})^ z#SF3?Gi=;VYucWfnHZmt{derk_Vo0zMC^213{>rtS}(my9*{PNWPFHO8p8K?FT5(h zzVqYMp&q$3a7i{+)=9`2kylT@BDLfbJzlqI!?K~SaFLVGy#1z3$7rQ3$K`Kde?wXu z>g3Q4T8eno5K6=ZZ(Pf=En7Qe+m5Zq;P?4!-FQ)kL%_>ngcu=Z9LQn@>C^C^o>^q0 zaRfhQhG(3US)u!~8b{fb{Ow4%TOBk9l|#CyUN{#}7J$S7G#Cv9ZTv?Nra2Jb*&$S1 zs73>XbUh0*g#8(H^bb4`(Mu7!Uf0%ryHytMl0E85$OpY%}O&KK#t=1K$Ga4GJ z*{nNkO#KiD+f9{Aqe!C-31w$Kpa3Xw1nNMWf2a-i*F}n&B+(P^|UKdxtQvs#Vnkv{Pl_aJMYy49xt82oDk+431)Th+M+Vo_m&zDI2|ahCxw%EoG8mwfA@fXY%+D>0 zr8dQ28-JmNwK%+YH@Y&v?Apl@RA8C`DA6F?`f`{VCbZ;BGG5}5qrMYf`5HrT#U1;yz2``Oup)xGX zuaHY7f}qN2kfiF23*_zdm&I9uW|-C)ZbwcIav;g90mue>I64qz$DQxJ)8L0!?Z{oq zc8ZVlC9k`#$VPH+Xoi{F#H4Iz;$ZYlw~WtB8)bs#AFcm%HwVepX+&r`v}Pp-^9O2~ zmIPE`Pz54}YJ=RU-v{vy{X0uatEMw3G7tzxDcy9)#zu$-dy;3F98(Xb&MkxrZEU+w z4&J?AZc-BNI@k$aOUZ;#U(do^T%H51TM4D;tcXG#EnAHd(z(Ssd2sImIeOxxct~60 zpzBS=??q7OVB)ZWGrl@yq;Hyu5D1{91z4skaWEuEzP*j9iKP_jEFjw& zJLKHOOZ-iYEfCBmO43||DeEIjY}vBes4mcJ4Ou#OZS^z%^HC0uHOU1xP3?w#?2M+qWTiRiu+|+&Oe=iJQmN(8_f_b4&%M6cB_FcPp75=zDYAGW$OzPhv ztShgOzxnI05=!y_5IKZ5{tOLE*L9TxN6%j5k4lv)_LKKKaECOu)Emd}-*fk!a`er& zXz(MX<8L7B@E(96V#2VDvWFSrKY#N7liRoMkWx;Df9>S+(%QL=jn_^{hBw!qE=XBR z2L0n4YScL1so$%FM$gdF&w6^mGJLpB_=N!~CeU;OO_d=HCLHq@Ma0Y+DDaBI#+_nH zaG6Glc9>$pm{c?rQyK;2()mj;P7_?0SDydY3q}K=2}#q1ix)XzcRS(T#-@w2>070y zvDP@;EDY|@L&o*b8Eo0w#Px%?O)k~~@gm6l5>)4&?VU1+6nS`VN}8)0Io}H+g*8?B zWr_{`$A9=|oJm%UEDEhS`B-PX6in*8QZeAv=|m#P*}yqDd7rZ;m!rcsuXo2Ps(c6= zlj%HHy=4aCePxj5!2>=+Y~5beVoVo|aNg4-D6bKi-r3s1&W<4~>86~b+rmv{I(nLn z4P>f)%TeC&Ew>*czwumvszq3$aftNoP-|P->*e~@L0Q8q^k;Dz?0PXla@uHbugqED zxG5JB8^dz8_aeX~E{$6{x14IQ#7G6_Zbt>O6YGCS1Tu$ag3*^Qy8fyFy zlEdxP9w}Tcp;^l()f_MLUQ`95WQL;JB;^I8A3PWRVD0>XPWhA2)VC=XbzMJ|){sv) z{_eM4lf)9BQwIZl({S@wDsPUB9}Nw@y*=N$4+ z8qyLn+VjhnbUxkqeZSvle4fwqc|7FSP>AsF+K|=-Y#r~AuRnF9(>?7k zE6G{_-oY}**yOgmZ{^B>vCiUAX zWDq2gY!mp5(TKftMGff&n$8DD!VO?5x7z|4ZsjF8;^AVtyUz;}(P%}tN)_44z!Gw3 zBI?!i$|#B0Wm&}n1jhSh?S^WK{_u*Ev*9>7Wuz5}V$vsNb*Y$`R&*3Ujs&VBkKlWg z^oIt*a){IPB#q8t#ua*FBPi^6QIHJE4v+I{dc|Kq3mrDg3R6;13gNqh1a3~?%sKd6 zaiCWUU7GOAxycTB`PDu0$-8ft%Qh|sa9Kc(nkt7*@8Kl~+Tt^$xfA_p@?LElJ$&N4 z#JWV5EL$Q|aKP2)+oR&89SN$*I3yZ?cC|Zkei?mK1a58pZO9vsg~|6lJdZL&n1n9b zj!e1G*)0bS*2yRDxFt-od+7d06SjTj&AwV zw?Ex5C+4#FsElBMfWSNMr4WtG06w`7P-C3LK8>GsGFUpkCIOhOt)pGC362!Sd1cq@ zw0tg=19b=F+8eJ&L#tGppJ|dKZ$nGP+|jry)A+@T~hPi+z~K3_$xRf`tUiZx(XFyvwhUP>N5S$6k;rsJ7>io`o4PA-)8tFqo_<{G)QE zG#}LARk*h>2`!yP(~}**9FC6n(6>Ah2Jsaz&_X`5ZDEE64c!KPxCPvaw(L%yRiy{TKST)9l@ zdmH6S!$w)@SSBwUzCG9!;b3d9+rlN zGqiD-rFz*y_`q76jwUZeI+w!vBF4)ofIdxOd?pHh6y|l+M?Wf;-1<=_g|3xs_*k}i z3&dGgDcyx7;v_HisgF*fH3qY7?mHtBG0Z|Jbx42v2`Wn+(u4$2)2o=Z6nrV}!81rc zT9hg4FIg4#kGcp<6%Vh5>T{?jC2DZ6KQPyvTm>$a%Bqkg--Iob>QTbo*?i{_cmcXtLRX2#S?NjKV)age>H$r&|Tbs~wBP5-YA1S(tQ8W=J+sm^ zAW;Wj7YmT65jFRTX~2&p2bg#kP@XbN@ac^(Xv{QNq$Ks4#SHFG8JY=vV{B>0TmdrB3ZZut@$c*L-|*8Y*-h5ybBV5 zilJyZL0d?}6&rwR-6JZ@LUbb|=bhpiqEn-UoSWiEklyFF&hM5x({GoYlw5hP<2j&G z5GwNT*zm$e$O&r1$|;M$Z%(rb73J!JnbNNfY%~G1G&RHcC;`GOhVd~JRts!zZj<_> zr-4h;z#MYI8+DQaCi4>)p;1P>67&X-ojgfqlw&9?EBu$AZ2ws|%NV|$nO8o)2g+qm zDAF7l?in*h>rUk&Y6-Ea@f4?Sy`=M`Iw2(npdp>8udAa=diii&rU6OHN|E#uhiu8a zQmQCvMJ3gcg2afYV?XrOR+;D?5Nmgbgos2voElg&65s=0Sp(ql`YwiM#Y0d-=!{;W zNID`S?F;3Nzx}c#J-AWIR(?c!hqo|YrAa16ekMLTv#K2wd!N-z15l`rE;dw$iy8>&j3Xmd z)^H(~Us7oaeLWpwahP`8f4rqoW)F5qVoSGtcKuzlV*XM&SXU=!M}96TrfM0Z zA6k2tY*>mk(#tV|sZkFap6HoaQ7~c{79m8U6gnK?T6roljdn|9Q-s*FYoCnKkf65} zfE+3;-rChh9h;_n;uxcio+=xP(%w1C1ONV`-sWR;+;a<`V7_{{Y+AFz7nrF?Ott$j zbacafrMf{B=%tF13m8H3IQ_smusjCD=XQ|}9Fv?U z&bVP^k1Hqi4P-1+heY0!WV{ACt2_HGpketkE zb54M{_LDc#ScZR1!w={vH&rs%BXF?(j9gh&D9gA%sj>=k9H)i^eLT59TxPG7dC9GF zvc-;%D+e2kBJk;D=jzTRAeHf*XC!IE8XDBr$n+R}KQy74NPvbW(|OZX_$qL3m>T_W z{|M=XNonsvjEurZfQ9oBC@D&%Ooup1Wfuzy_*g!e216P#hd-p2014uB)~~jAXq%_{ zToU&20gMqaSo6K?8pu@nV+DJFU_u!Mh=VxM z+T10M^_R(ov?zIW-(%7|1fUVhM&^RH*u*R;NUD;t=^}aiY)~#;o)21gQcfHu%_OIu zAX*=xT_`^&OZb zQ5$0Iu||YlA2@l6;x)d;^pF2|r+Mej{egQQ{NiMD^U$qt>~D;ir7)b6mu*z7CY8RA zNJ^xihR!y2FWyQHk;tp?w<0Ycx}r)pmQW>X?UG?MIbujHY`}dXK#^FgA3{OGMK5iL z;Kv3wUE-$}%dS1c!WX*cBxmIUIR&u(8Z~80Pd`zh$g$V9%V6`6q>^t3jlEEEAn#Rw zT)NJWNfQ8zADLp!f0gDFIy)g%S1+Roks3a!L>b|)8c?ZEo@QfeDy3B&%?3t+KouAO zwgVC*1!RFa*5L(K{))=%dw8tsc#ZP@C+T`nN~$7cDn=GwQWjXBU-Ra$Bge4wp~S}T zzUl0iQT$;Fz=Zd_PB2db8*s~-6_v7b!6Ldkkd*+@%E-#6yP-tpFI-7gj3ACp9g~oa zYeCA1ir5^wODfsibiRec31jHVZkotk&LPEg`=@RS58V9j1JAH&$qZq?j!sSnV&dY= zpmy%6`ITSXUsvZ^w{D%$3{F9jOXQm6ceP&q*EbwWNJ+UOBOQl5tB~I;AjJ^%*COCI zOd?YJ{XKA>6H=I5PiC`z9dwjp@f-zj1yYNHtgY0RkhX(5tkMmJnywM3rb1jujZH`B zw(EsIWI}DmvWU)x^-JXKv0*Ap{n9~aK~zVIB;;9m%nu|$aDWDwH$>#rOX zOA?eBHjIxVNEZ55hVe*taU*={a;!1FRogDH61up0(@0g(EA8NmhGLYFo5yJc^;IKa24^tD+VH953)YLc&%%}qO(`*V|*Pyqh2?4koDO|jsQeX@D{c+j8 z^I=)Le51@kiE6`gYM++}Umwf#wDsjHPT(a!Y$N1E`5xn2;XS9_VTB zY+dcQzul&vegFRbK@>1291}|sH$=$z{1?1t{$)|9nE`t6IgBmQZHg4%6E1gB7VD)>dHL09q zRf$)^xGwA*zIOr`vmg44@~WJ$C$#XO+-LQ_;Xsbfql>FI*8HLm)(yqyV3Yy@Sp+ko zCgHQ8uVpV{Qwc0cT2}x)#({!Ev6T8SxBK@z8SeW{Dyf9aXq6%>%uf)+>YQ9@uWOJk zw_eAAz%{^_NvBfO?;H%XJ?7}lc;wM1K(HpHIA;mui3*uB%+k0&YD9$GFgNG>H|RUZ z(aExgm1y;iCYh29v_%9vTHAUzU3>Y*XoR*iJ0V2lv>}Z*W@X5g#SV35Yz8E%kpe@k zIBxv}(9@_??s!I0^0UQ+8JspJ$t)Mm!l{?+5*ai3Wa7v^8R`R85a>h`HPw1@75vast@8M3Fc9P5J*l7d z(BU{skzp8Wt+FoEa5s0g!i$1`#>rN9tCcAYxPBe66I|&kYbrLy>*P?!*11wc0}$fl z7I93>l8^X7Ap=MNy;V*JcV z1T|MmW|hv_FfROhxM?jYot&5j?DB)89VHBK-n?)pbXJ z+;H_;W+YCCnMz9oXqqiI9THtBCy5=vi3%(8@V(ZBFIsHPV`@E-I8mu5LAPv&#-4Mfx)f+=Kk*eONz{L_0?-6o=fcpQL|AwI(~3BWlWzLln4CICktZ!Vw6wa~ zNCVo=Gv}HgzXRpq5{*e? zSIENkD9jaANIlWp=7t`bUc7*5WgaPoy2<1{ly#6cY~vzvq9Z9o=JA1(d;26hKU0R$ z+Io{+K4*%SH7zbW{g5=GIB-=nAOvzoP8KD4az@6}+;Rm(vs+k|q^(zqv8`WjlrnKH zzfuyS&hb9b_b_G3-lNAzKS<|jRmlQP`@vVg6z&PGURX($i2PDbCuUS5Wc{mtSjlZ+ zjuU2Dd!2Mto=ywZ9$al@i6^3N8&I&pAo+|Cz)O?P(6+)sP> z-gH)m_dh44HC5uxPLt<7#SkPSeIclxNTTgoamBuds^s1q+>zx<#BqN zqy>E1DW`<`^RjE{B%#W5s0n5v7n}DMZg(kKC{OHoTKd_DR=^D(r&rBq5DTN}ae(TP z`bzc1nx=)zh@?Oqc{pFteu-8mcwgtKf+Z3NDd}zsT%19i6b*QvoZgG_h#Ec5vbao5 z{lMp?1Q*qPZXZx$4wF;oBtF6;mu+%LaTWymHaO#4X7_NX9D32b-ZuaiY&C~<*V_|7N_@n?io zGd(RI{JA^0gf7iX)~`FjwDY=9sMd&bEj_OnZ@*q({js*THvDt>e8+t{6soVk2l|UK zthzqp9FtVY*YO3})LH2eJl!O%Q2~_VJgCIB%Tb)ZA3y7-tz#-Z4<_LZ*eeyIOj;x9 zfG+@6ysOhK*Yesm!{}H*L>p#O>0m6}UYv%P@}Qp&=HvB@v3A1d3*vWVOCRO4apptB zWa2LF$r@)5Y6d!0}qr|5RrDj{v zAKYp9^o0j;;O{P$5E%r`DW0G)dziGygk)n9Qc3(Kx>{4*ujDq~mp-1zTYH6vVY4hG zz)4L-Xq4lva|Pp6o{`ra^9@TWrZ>5uTq5#Iq&N>$(Ago=^}Ui)>>&4M<|3wj3~(eMwD7k-N;yjQe#B@4T}=>~{5gz5Uh?8dmF_dtQ92Ij z%mc_3&UN)mX<8ne*C!WVtK)=*tiDrWH#>7WRE6s46Psz{A3r9yPVxo0k zWrC_&tLChl)3$eXNHaof!}KK0Pzs%86Eq(;Z;&qqv)J7X^k|Pw+H; ze?FxC(wdc!5}_9lHy~tQ20F)Qh(N^v^?|4#TVsa6HT|;+e z8Z`iqJ*-WA$+gvX-qC7hK~EpgCQwp~eo0 zZ)B<&{9ssnl1~8Dwje`(R+%F$Q8YtiqK{B)AK_%A;V*j{9zQC-e5sB!gs2#ZF&i#;AH3_gyPCy;&rF0CGlO65q6<;$!gv8JfnT~Q zrIykB7H2$EdMRZddoS14H9= zm^`Wzd2`963t9ds+27j%A&j9;QSfsykFzw7Ioa{HDWYsNs4bChDNcnSb)iRQxi}s# zg!7y<%o;G@1?DVe+U3$^!RTH%AgFN++eZJ(*GB0*}q2^Up{c<-vy=sIMyH z!p2FN^_#?D56KnoL2JGgjrg$#c(1G4`1mn#Kk0y^Le&_f~pke z`RbDaStsqXY#hC13Km{A#27S~IxxFuM!=4H=)^$iP zJ{w!l%GophyeH_d5b)z*LMD1cDd(^{{oFS8FKnpNU5Hs|5&>i3TcoVlT*jumS60C6%@>kTFwD^blWe&+~y>d z{!+OX#~;})F{@U}fGt&?7|VqFJtUv|1Wl@2Uu#c2QPU+X4$Yv0h8#>wAy9#)Rvq$7g{a zjh0|Ei?soY&T>mUcr!Ya00T9qSjjEUlLn-KTiI;^hyWI9fnH495DbktPMVwVTjEW! zcV4zM!)cl$Iu1s%n_*mLKXOQHM23aHa%EiDlV!OwaCx$r^YgF5MfUXdNKf-Q$t8tb#n8*a$6uC4 zg0+b$)b~z6qQkE$c$%I_D=QJ{6w}0GQp1HROaT*d5cP zC%0U&N#a9P$O5C1QuME64@s2kCS?pa@zPTd%J&{RC|6&$9u^4`%D_Kw|NFm@ex9p@ za^S%JeKNj#k66kJv462=Qs_8W@H#Ly2}Ng z?DELj);4MA=?zQ$h9_>bh)MWF{r@2+s;$HWfIIkWROB(aN)AcRVGeN`L$ScgV=Znd zGf8qu`UJS~SHwy*pQY)T*Dk%&QSzy8Jtm7U{jhxU6F13*wJW6yK;~PYx=k{x*U}%% z$N;QD>p#^>4rrVcnDO?pNZGo`ARoD54cN3>4!p1@JZ{HB3__0%&EI*9I&jM7JD!Ci zWEE#GYCa3r2db-UEWQ21?z0!Bw|1R-VO*XySoiO&)kFC^H$B$xdTVTyxd#~#ea}4) zb$<14H+;pCmT(_FH0Yc18QV;@MpPvR`{NSN8KIRvZ$MzoQ;7@qN~ zfE-RJ3X-o zfH4(Z`;>8Im}(>w;B_4uX9fadpUYYNUwDnOgW=Ox5^+b8J4V95T+4_7e{PDrkd`DV zScu4$RvBm-q}EK)0dAR}_c+99iAe&R#xe~9nR-aobg+iH6z%t2S)3+mg?2FIvoe3n zt#Hy(>5F!e`WPjC2%@3nr&rd=aZbW|Y zz@st`)M|#}L?&*lpb+9=N5Aab-*m4GeDOljrUz|puiW$Ny!XE@=DV8ccOJyz&VjTV z{`u}5_Xh&DgXkEWUFdkd?HZK5w2~Szk(ai)q#De$Ga4-pr2Gcxy{TY9Y(RiMqyfik8 zk+gLD(*fwFP(MQ69*0FxN#btMtYK~B(hp8kmu3g53UIcIt`3O#iP2dSbKz~Vx`t%M zWm{eW%Yz&p&@oGFEP&QkTv~qHmar*+UQhQl9Gq1Q|Ui z5s6V0{O9?a8~aVJo`!=g?)?;03~jHt6rlROx8Hk$fAt%ouhp(MORe}LU+cN`&4Jc) zr)MT@Q=Vymv^~z;PD6KboI@O)V^W0Nk%9(el+H%Y1ST`oT_q9JOWBDm4pDS942%>{ zm$(URLZkvYT>OEeTq(ytxq?VOP;7jtT|5g4%ve5#j) z^>L5WcSTQcYAz&v3mXTTj+sqhAs3$kJbCFTihFx|r9t||v|&=x_MQ;S^Sk8q?YGF( zC5z>9&94*If@6bSBXQ)Th!NsF87OD!?8PTuctXDTxqlLCH>!Xh#sF1S%afA@G9Uka z1(h)`z)Iue+XMtx60X=P#*CD3kJO`&ydnGQh)c3ynbMz~nVlPGY96$t$4t*wEe_p! zx{laj$(^iU7=irNa{ca?clE))`LO-_JA(>de|7s0M(MlljtI#0vt&HUkeDihW`io+ zSpoaa2z?y^u`09EeGc7b8l=ZFW2O{_Qk&l?*~kVb5OA5G-9a}DM?**9Sjn`Ozil+m}qWODd1eBmlEM(8(HCAaPb~M6MttPgR02Qfk#yY zC63sjI4V|(@zYCy9n+%_HYZLPg!MQ=)IW>j+F%butl2P8N()E(s1u8ceDaZ3oas*w;IXWD%^^!B+1R0*(odF}vsG{<7k-0fM&<9ZB zlJW1+#u3Kt%4`E0c9i@&-uOFh%0%hT{^9*dW_? z_Yo&huqY@KQ(-~)^!fmRCIM;eurbqUsOwL?T0S$CDBrm$Q!ZauB_24?L2|IkX=Xq~ zL13gYFM{~xnSIQ0GQbn_bRaUflyov>w%FkXX74|?{Rg#o>Kbg_+NjXLd%9h8Z13Tr z`#C?)RBTlMBzQ|w#&vTc!$?%5-D)16@nLj1g$a@qKrg~QEJc}oD5bYJQDmdvb~)nU zs_+`k$jKOWfeoCvuH2PE`W-AUZxK36GL>O&hFd9Of zTr(G0%YGy5-5zrgsbe%K$un8&@Tt*36{@;!W@Gh3t)0Vd2Uv9tgN?A%CI!++wxMT$|ByFVxV9xJ+ zMCPxBXup4#M4SeYu;epq33()@oy|3!=0s9S^MetD;YL4lP^5LVlfIR_LTZTIuUlTK zbw?Tn*nH&n)4YEJ5M%~2h6i6ggltp?)@mBdawC!tMRSP}oi@L*d-#~se!X7%ty^oY zJ9qB<9mMMQE#Z4!V10s{8h6g^F4|;^dhMxxvmtcV9OJF+zL3R6M4@wjRdu|JhFb$s zErQFWP4wY7au_{hmb`&pDTNc|!Q}ZU4x}!skRI^bB=30_whdZ=Si z>fSsm3~-Z@yh3O(g($D5$XjhK5(3~@3o$<-CN3OBYxy$B<}85gZ0tNMK2Ecp&5M4r z1h-rzS)aNK3&A|$2fr@SNtM8<7D*x6VRb|bV@pb{50K(E=U(QyEc`j@-8fq392a3MBJ%4O{m%u?6E-%&~CDd3nb#w>tqgjhB*+twvG#u zYeep%Y^@v}&yg)@bBw$%lExEFG7ASy=|jU5?=!QLsqqfU_ehD#0?i>?TCO3XXpPV0 z@3RCAE#k6RwjBH6=Z|f=`)=F*AJ=-rANCLU=MP?BeeMcQa(#8RrTpmgy8?rgH$z^f zA7wgQj~6KJTHR~?Ccbza~$Y21}E7Dm?W5ieomtuvYZ{PT7Pd2U||uQr~(_O z8}hurwN(j4m|8?ks0}+Pnk&6EMEVt?-W$QjN$G=SNiS~@000s1Nkl}{$mfg7@jBhk0_{0Y(+Cmj&|vIsL<Fvk{he^K$tt`AoT*^h*K&J?U<@ZiDd`$A4l#X zn$?CrL`{04tx0k>Tp_E95~UIvE^Gzq=5*PSR`9XOCV@(`9LzREwo$3G(Ms%3qp6+u z=mYt(SIrlK2TAK5Ld9T?+!}5bZ6_)j?1mXxQ8*_zUzG-YnI}X0_KWBEAxQMZ2t>{v z_|c|<%Vc0WlhhHG2W8DJ=s5%5xmVK5OXwT6Ad{3NhwDzu&mMhKvTU7_l)us!6BUgb z)HzgZ1GoMByPtnjQ`Cp=d~Xr+yDjwlUSNH)>O1c6*Gsh}|IvqjJZSXZTnhWI&>Ce) zin01;h)g5tHOj6jq2n%3vJz9nsSF6b*mIcgN5`U*ld~8Mxs>XU5M0d;w2AxpVVULP zJJRE%Fdg5=+1EyH3qg{f`=c*FN$nm$GJ(@hq>>M=ooYjH1fWBSaIr+ngGtd6k(|NA zH6TeWgPRSK1m!j|2?alJ$rv`xqKE|9R8vJNqC1C^i_cC;jh9q@-$;03W6k;TXlqK(yX{J4DWYv)mlr}iBgKNpjfg2QQMBM;yF#5e!p$9Gh- zxZAe9E5`h8i}`N%{ZR`HyFXMNs`qch`mVh98&6J{0-FajV}jEYvzCUQmN^@Mz)6@H zPjnoW5S@1>0xXLMaT8<)+o=@6B$kzQ<44oSbU{T)PS*o|&?3pg6_X-Gph6dRIz&*a z0cnyyD0h+V)QM^cGjGncVB7Rvp^=pIV z0%qtS%1DNds1kt{vY-y$QjIEG3>%FUFWAr|zKm?DS)6RxAkKTP6#K64%i>kYCk`OY z{qk^10gGzf-OD|VS(|$l&5a^ZjlrAHp$rK!;IhwoHyNkf-E>h?!3@H?-5Cu3?gPkw9LWA$xKCl*y# zl^;uwOSAR04F(!#NBs%PlAv*111tl+ZWMyRLOKZ&;b{!JV|uk6W-1ARaC9hp8ZJN-V8V%wz=(oFkW|PDNH799F93QqYk)!72V6*QzHs#l z#sd)vXDt#Jn`jJ@pq=r=asor|7&Y+L=sJU5&Q{ZS|P zopt$>EwDbJ=E-*x&hu^mt>5%5pFMtd`$9L8c*#byF~T~hBU{rzZoG(up2g%=!p=0s zGTV26dAzSZCq^i?rI~5r9-vuGZ0TIk+*W5M1Ucyg6a_qJb}S%|)!L!Xw2#NPdpJEa z@SthFLIaw1lpzE`bBo0UPMzo*B%omfXuB%7H{H2XMu29fFp>dZO9jQ1mZK54s_mF( zoZ?$v9Ucu}?j;h&f7_SEBbnSHjckHKfR9#Og$}!n_fJy!Nhh`lh-1|XNnue{_j!Wg z&*w&rvH*LDx z_J`)V?=0l+`}!wcV11E?2!onEealA zR4iFspdu|O07G>htRol-aKZ7Jq#rsDNK1o^4k3ER1XC`I8JH8}u`XG{!p%#`l#?xu zEH1Dor`bb;m@4e~36`S4xII}};!Dnw3Btv2HdPpLXlnx4!v_ z+FA+f;=c0A`%8s>-{R{1fAR&^C%vh*c5Yv7ty#dYKl`0;{3K(=@^a#-gQ>)5bkkdX69o2^du6bmblZq%AcrP@&FmJ0L(P*^NWici_hlp1PLMoM3 z+cI_5fog6T6)ofUy=(^hK(HJLp%G+F@x2Sav(7?dZmSqcqgH1R$z3-%pvf{riK{r+ z)CTWsA{^O=09|61%pgDEM)smxPCAiNk4o@$wRRal^p&U0(4UV145CwYC0G?Y*IG*O^F#nHAp1muYi1z+|;%dIJ2pgb{7 zJAF?;q_c^1E5OV&!R8zT{wyqoX5*uqFBc(lh~YD^M7r_m+QFde1lnUX3v-cfyzp1$ zWd~-6>ybjR@m{pka1P%Zs2`QCQMgO#Ij}epHbddh{ni0V&dq{=mqrARINAwd(5H4t zIqfSdDjp8b`Y77ZSVH}SUEV%#jA`0uvn*b-WGX2=^|RmjJaxzCR(Dhrm~yE?`}e;) z-A1qDAA9?=EwH{ZmMV1T!w>svYuBNd5HhU!=C|+v^8Cf~Yg(J0cBB-*Nj65bKlOaT zI1(f3aT=ysR6>&!#tpdVTt0RM^p3F088^%FZ3YoG>G8DjDv4D zA@Z3Z7j$zv{ovpUW5_asaLrL!VjL$U17I~$FbJsv4gu916(4OY)iJHw+HD4OvG&n~ z);)p%8Ve5eI&d#!^{?C|o7XOuL{4t+?&s;Z`iLBH*T`odZ69a~CZ1ludBgmF`rAvi1HrKMnP*JL2~7F^PZCV&^?tY8 zpLK!XdBNJ+{RmtbWcPy)BFUc6`NfSN_@Fgw-sPQ*r|a@96Xv44JX>;t)yIfApK4GA zFwsGRALUHXkmt6ceB0dDB?;uMs-qbqeF@MOVq6Ytq=!3DySxKj86YtxdGqQlrc< zu^Dw-hs%HZz=uD0{bEDs-FNRM2o1^3n{Il~6`^;oBI}Wt@)9m(OJi;W}fX%@Mt)c;JcpzN=wJ;#Vz-b)q>lJfxnPfVWCB2u54^$dI zr?68z$OAG617B=3gLDxnA=b&sB_f7&H^W~Y=w+n%;5|nZ98qv2U`P}ybFnZ#N8Uau z-|6p?!6KAwmMo?3UVj@Sz~qwHXmcoZ=7e!xex5lu*I}4&&mNfUJ9pi#dp9rH@z6)Z zi(3IcP|HQAt-a|FD)s&5b$h?x|0PfGuiqoPIamv*6=GNKY-}{#L;*Yf2hYC9177U> z*ya_}CexjiSg)~1CnUP9sm60?faE7+2BOTi+0KDp1Cz7iz=upa&mbwDDh+`cy{=*= zm2oHKQuzT}&LIy*%BT-^q8qtIJO0~3HAGeJREM@xR*pYbBk+(_#fE`LW0gtA=*jTeQE8S%Fx0~kBjh9&2(iG1v19}ByjnSuhI4pR zq*Z#qE_71noGHKd$KI8PUuxqn{ktu6_1`26xJMTR0+yxZu{A>mW94v6R$lCIOGHum zDyxs;eu^-ZIwxOdMga7ylB8LS2+9i3k|LEJ?U4w<)YB0Hs5*Oeznl#(6z?$ojc82SFH*eSbW2-{}|&j2Y&Tesv#XbIO5B^(DQ7*TylM3 z#BhH>$mMkO_zdaYK~G|uH8n1JER-=GnXe diff --git a/site/themes/pinniped/static/img/mo-khan.png b/site/themes/pinniped/static/img/mo-khan.png deleted file mode 100644 index 8c4d47f94dbc7d2d6dbb5b403fd9a6eebdea4220..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20615 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

fP8@{NYIJ{3_<062xEKG0}q@`$1q&) zy?EA`L-x}@g==ux7{^!=#rcbab@actx`5)NUX!qOg{kBDL8Bi!omN@G9%*4die}DGu z^MCN6$DjOD==BA{RygEc1<}3C$@~lwY#LkH?jCN>VHQxLnl@gC}Kb)KO}wbmDJBKkb9FJ8~sXrue09%PelE1&z||#KRS2r#ZR&V z`1gy8^Uv6(avR^Se*5(!@1A(#iH-{wE}Vq5pI%sAKRvYP(CIyUhW2p_PtQ+(^yA%6 zKJnomRFgfQ`OIJROmgOTV4!b)dU|GlW_o-cL@!|J;V;jOjgF0+9UC1v`%Ay_x6k%H mc;e;%;qU)_Ch8w;_x}L}g9JVfF&e?FD+3t+*@yr*C~N@Ce<1%Z02DC*?!RmRKmm&6e{BsY`v1W|0|3$X0NDS* z82r=!X>$MMU-bWIn0%=JW6X#CU$k#N%>UW{342-L|MO2FxytE#001aB{}Cv_w;aNM zZ2IyBZ~O0YdfMCkUnUoi|0(NVfo%VI!p6bM&i23D|3ZcT;}ukOx3~V6 z`9J)k9K!#F{Qvp>Cyy}Of0F-y2J_#Q{+IV(siMfjZ2voLqR2}9bou~*I6xjOq2&uT z*@e_uVr!iLlpFZ-MaN&;VM=9b7!z0lE`}oFrP*YhFKTIUBh0~r_^iX{xe5uO^D=`e0Tz9(RG40cR82IsW z|K6kZIK8bAjZUTz>B=8CgqQXvbnz(u&@R<|GcDxt68(35wN)rX;ZnYAV9SK@T+Wu$ z(4+nDfw;9Ncis$!jr&um7VVemBX`Q|iF?kkp3M}JJl_@AqO`L1A8&qRsys+t`qpiW zZ6WOr-p&GC(`YvYmZ!y#;>S(Tv*y~L1NXdOMcC#`4^syw@J7Dn+TF@ z8+j^T6oEq|TlF1K9Er-CUdzU-b8~ZBWIh|$+7Yd8lVT4qKBQq((WC;Y9e-viv*=fY zo@|4BAI8#-yZt>q_we?mH2Ls2f3tiI5cBlkoISIVDuW?@R#r=cAN?JKkvT~aSD_@B zJ#LLsVbPESol;>`i^+*8u26i`xKp;xhL0TB_P~^b$ODxg|H*z(0!s44W5}k7$l%Yt zpLcNLFVVHJQqiY1ipS7263fnqE&nDD4-dJlOJ;DeERZlnTE&;O^}SE%eL~B*;^|bY z^5*SM=5A<0F|MRyOde>8w5fCA^HSr|`V0Awj;>a5<5v~N@5*Hc-_ao_VdCw)6e*hY zy<;FgwB`PqCk+7*u^^&8|61U6#vYOz`7+%(a$5fWr9H`8P3m^ggK<%@Ok=Ea-eh^k?W~k4S66+xGNBFfZ)3|A`ttNA*_h`StsD=h;g~#k-;V>NyLI zr%kgC+NMiY+EnIqnx^Jg6t3g>5_ekM37QZx$-oj~#z-eUqc%+P;8(@n+pkSPPc*U( zG+m=WsWz*T&{-!hDFW!qcPGm2%L1X$THrVOHGJKm^u@!N9)=My0T0zW5^7!U_7 zH-xuzlPG=s-%KjEFONKk(0|+dCBZKK_7vMZyW4ty^VxbT@Wn2p?B;6S^j>dbJl=c% zG_l@F)<>*5eV&Lxy>vzKE_T^gl&A7xRH4>kvSYr{i&}uA(WS(?tlG14L=ah!a(&hJ z^MhXaX^^U5Eke=!hj$ZAPozg#F6yt5Jfv_OL&@4KX<@c#87~&22V*vjmwRI@_~-O>#a23h_AH!n-UUj!!C}Nv?u>A z;g~ci_>xM{&TwhBoBW#&G8C!tMTWXeynOWjC&?*!MC{2AcR@;Qt&ldILMec|1MxCy z$0#tyAun8M>puL`=U=9N-4-wHEm1Gs8SAZZeFzxjljNcS{ITT^Z%ZG|jz_PNrcAiV zKDdnn{4QN-6L<~Yb5#Y|j2VB;yM85r9#Wz&(1P!O)AipxM5&Q>Q!`H*hdA!^&mtcx zAgM4#^+BPPYL(aW4D|PN5{?#FHl*&_7}5RUQMsl;5WmtQ%-g_*j~bS2(TxyVECxmr z7?7@$RQIkL9Dx=n4s8UC7A$bU!Y4LkyU;4d6RRM>aM3Rw(!!^Jqdx_8Qet^*R$Vq# zi{051ALkfdPg7-`j|EcEoSof=7VB<7imgJQ+9-XNr7)yx-!fWXA608=Yv0R)TD5bt zg_b8u1;=@vwl%oJ6b=LQ^bOHv$)BA@IV+Xdks+RhN5nc8->lM>Aw z-K=U=wNFYw8z_2_1#?BrjVYVkm1EP&VEz(!^>$%UGR=b8HDu5FW=`0}XyM@cXLgod zBR_n)iV}UfC~l#z(WS)!APDOTdM(t>Ny=F@sf%V=XT=cm`GYPMUSw|Pn@Y(jKl|HW z#Llb94kadU$~NV=8=W<4ql>Z;r;8LLK`sdv3d<@*$vwD#mptz@+^W1io1|PzvTLiY zF7MCB%Ex17o(MdF;)KW+&Z#`>gYwG@i^Nr5I8;jffu;YSnlJC81MhW+Sm@=vE1PIqM~Eu%*R?Z3f3>fB6sVI(v2G`gEupiS%lV zB>Vmc1gGK3eFPW*^|OJJP87fanQnBXj7vZm7Wbo4)*1~MJPlhKvSrWk;)qHu|<#$3^X}>F2 z_J1p9%M5IsE9rgWM%4IG?~VYo5WnSQ$*sK;>shW{6|hWiZ^i&a7*bVSx4dl0UAgB5 z_!*AU)O{f;6@&N&Z_(b4-pEmnDsAKTb%_MPLuO3V{PuTEfjU4+C`=UPsD7OMP2 zjdDE9&tEAm53Oz%QR0v3++t8A259KKQ0}>6I__STv+>=|`7?WFn)mVbp}Y01g(7e< zA#NgJhWm`^#c-2bX=O-UrQ|0WKYp(_05ch?UauDq%$iNg!V}kKP=k+^PI;x~_ye;C z8bbwkR-{b@t7_UAr_-*s)wg+<|J)%Wn{qsY{B;Xv<%|-e@bD2H9C_9sh-jD^p;yH_ z@Rw86V<05<*sFi?$rAF1#oXOpI8owvNPZ$1raSe4K8%f6TYUx_zw3nVp)D~d$H|qu zXP|ykbqporQ4F1Z9zU!y&!Lvja7Z$Uz4t5%OP4?!-DFZ!rk2ZoODv3Ap*x`86aPDUW#>DlXi7#) z+ZyKEhIw&bcynlx@&#h7g$Ow{`!2*s6H;hsYCv7tw0}oPvAr_%X$3mgew0UMYSNb` z=YmiYMLs=xg63HFZB+GR&veV5Ev0+?{kPy#-eg~l+g0xL&!FGIPrEnAGr=cu7-Grx z3t!et=FLo?rw_%E5si%X(O}wmVZyB`@t~-Om}=p%eu{J2I4Ny6V|FpQ>cX`mVd5;D zMk33d^konhLy<6g3ebjMkPi?#?#n!pQ4;^fl%SxhiH}=|=`ety=LlH%cu*rY=$>CbKV!4na-86MvYL1preAu(t4weiea%UE`IUhN4vK|7MvDLW*^-X z6{vsU^iYmJW%E(Tg0BgGAiLdAL!fsH+oZBrcx|MP(CPwz_wCm`FMqsHfu0_MKdz$W zu4q4Yre5BQT4CFx1utI~^FA85Y*o9?CQO6ab40x~2yKudtubTj?hS-;Ku7C}SoE;8 z$689zl|Lb+v_~_bSUk}ikk6>X(_2`ggsWJL$-G0RHNX?X8?=<1qIsYr?S$>96Yo=` zzNj_h5kQuB(fd`IUx%!%!yrilTLYp#=~({sjg(>p#1@5DeK{@;XJysTv)sjgz7u_pb`y;rn15HW{a8XF8hkNHQC-cl`)%Q8@H-vF z6sq#BKjVcABO%Lv0Sv&g>O}F*wl0diTKp-Ni=W6rt`Dz6*$hy^JSpYMr4nYlp*Qm-T}GiQ_#}M^b>YJqV&9En7jY-9%KR|UAqqX8kMEEHd5B3 z%N>IPB2L<(V|BwhVjaY!Rz%Ex*$kOMCAG4f+LVONYygZIfky|+J9 zrd{%TA};SzbiWm0cAB$%^nC2Hu$}!NrZ9%0R||Z&eY*J?O)gBz`9#@qt(;`D~)GN2YcE9iRH-#FU2cD+|rLe;|-Fs zreEOdjur*`pBzYvAz7u@N8yD#-=~ZmBi0%}JIK(TG1idWV(v^*p4wqKhK04W zQ%L2^japDd4=NA3bh=U$QFASk)i|RzKW4)dwVHJz=H$4nO_)Ah2A#4|P)7ysZ!=OT z)IKMCyoY0ThHiX7nl~K0FX;%fCv^WBD6j69sq~ZGV45@L2y!Sp%%CbKjcQ_1IbhDk zK!(-SM=CuVPc*S`huCGX?fE?EGFfwCwP5dRqLrD5H2C0X|Vg>_}0gp{H>BuAhC{ zj*7gpJK-I{GW%o+*r?;{u8z#E1b7P&|etyNU?&Y%t<1t~0p z9GY-QRo;W}f-KQdp#u284#!A~N*)B=H?4O&w%8|s6&@I{q^p}?*Y~22w2#TaBxw-h zYNCL^Wpo%|QRGVeF!}=l0p5e!6#v`Tdmf+H>^xB)j-+YW_n`Id^;l8Q6Se1A|E+Mr zvog_#n4~~Z_V3Hf`|ObNw97ucDp)Myvu3%9sbK?p;9o{?yqX~pW!(BNazh*~UC1)A zN;GmIA4+{3(0nA5#T_&D^laWC--KyMwya4xbYf84$Vy&&6;x z;#k^dFN_~}0eeWA)fbvc1ef_PA^&H`+R8;k4s9foTb{1y-SDm9QTJ%>_;stl)wFJLKoBWKrv@wY z=-AT6$=Qpex?p@s=gYvgZ7ytqyMV-Z8Ik^mO&4pkT%C3!^7CpR(_7hx*TbJa6rxTW zf`N}$yK!M3Bvj9r2|M8-$5&%%T}Qt#y8V64b~3=Jz-k?Hd%wi|J_T+Q*2zgQtHK6_ zsALo|%Ap2_!B}j?N!8vE%rnH!tc4vu=@Oam<{DS=+;Y(kDWUcL z59qgnY@HuBW&J7(u|qH=3IfETmhrY$cCk#EDU55#a`1?Y{66G(X|meoyz#MK>R1V% zsc2;)qPM~x4qU0_g|Ksw@Dc_pDk|8U{YU_o2b~9=EG|+Na+UG*ttpKqkQKABSbt1_ZzE8LjeQ?SQWhi_ykrjo-SVzcWXmN0@L-26sQJ2y+qycZO-JPWGBagUEQz0&hF zuaF|SCLDTFqak5k_mN)_5x@_6*kX$&8+e=`H66e2*9#4qdh})nQUh)#D9vY2;}x|- z@seWYIb@1Fz4}X?lbujEI5&_zPJ#1YdFkYEkbJ`3$}zF0VDT5j9;=vnIVu!NWA*rz`{TeH!Wr|Uv&%?DdKN@_%t1%$nW&bqyD<*)^5 zCFC1*mhl##SyOVXnS|Btn+ zDpjG(Y)n2yBKT9dlYlmr?Q9Oan|YCnWtwI|Na#`Asv!wXKNvIuO`5U@-;Qkt&cXEd z?mUa4eiox7dDDN8?$NBp8NJr4`RvIhi&0{*>;Fj@b1UfBvENpel`GFh&&UL+O7Yi@ zI~v$kba_ego)%kMQW37W}pS#Y~u%Z%Brw|&uC8GqsL{!?3HIB+m3M@U=SiBz0K z*{z^t6#^EAOIbva*G$84{fihPM^&q!4t;=WEWqCm-@C*ir3v7J+W~WL(LEfti0dDW zX_2s{oKL)UGb!*Tv!+@O!54m`4mfc4vQD)8Tb`#Iw2ZfVYm3^Sv+M z0ICl>0|a}OjKu9g__4;su@jY02Z{3M(WK1Ea`(}_}x zb|k<*E@XvjS&(8$3nfC!=1t(hG9s}U#Tj&KRZ)(b0@@$9RH*e4Dhy;iw^&k1od|j| z>{W&FpPQvvv<=P7A=?yU;ucag#2~Wm;K#_(Rj=?gm&eT^APp0!gtsSBjgOQIQ%PX#3+CblL)em;r_s5H!MzGl23$qMoH&EzV5 z2V#W3&rtYnzj!lL@-gxX>^ipe;PjEmmNq__Rwe(C#%=la=Kk%_(A@qvbmk3d??j|v z;=$mo-HLRr5BoZjWk$eC#ZQ=MVhd&i*F^ip>V1`4Lf{?>ex%SdAuf&kk3}`v*5)ha zl0E1i+0D}J@LJwqMQ~0hZ17JUnG4tY%tg2tkY&mDecdoEz{$_L1m!&b;fOxIoK?pRC4eJA|h3AeWygnP|Fu*BIJ+flGbRPkz>g?M{Wn({?mQ|Bic%S zfn`TeM92;`gE;2y8l~PNhc8THo34f@Mwt*d3`4wt%nE8M{gwcS0(S5{arYzXLpY7t z$ag?tQm`;Prva`0gX6`!%=)r)J(SB?=H*k@b{iSNaiT%j+`deN&(uiE!0W3%Dnj5k z<8stE(tiAitQBCUpw zf7a-0?|IgjH~rR~qk(Q<)7A$j4Gx$Kj$WrKeT-waoVYQc%OVN~1tqMnP?%CDqDmIppbyK%CaLiOseSi7^hwjP7V$ z4jew4Gm0-pXNxcVW(jny^w^XLUPZm`Ug1lDVAy@- z(P&DJt-9qD>h8ta!Mmhzb`g_YX|Ksa_Zt{Up4L}_l1S}7kYzh}TGm|@X(?16$0_O_ zwXImg}}dXZ&!86415Mc;wV;*wU82L&<~l;iC$3f1H||K~OQUa)_{RCk_H@ z8uOFMj5mAdUCeV9o^k?w3fsmB6*lWNoCp2r!{7>gVqn9cl#2v_>Q*gnDg-L<X&Ld!;FH3@VV}Z|@yJx5FrNH2jD` z2mSXIZo?r7q3^9S)l(W@mA?I;BZsTnZA*tuE$A%A7f_!2WF}iE&rc{He#GHlGI;d> z4s<2oE=t7n$1M8@c(^-jh zUTvG6ArjZspf(>?$@-or!8~K}h$_wDu&ITA<`Lr=FQ;!{khv~`Pz*GH@{76*6u?xk zjC87Pb)ZEA?o&oFK$FG>q2Xd3*`_$-GR9JY{|b(l4lk*}gOUYYbKYy`3VBP!b!6@2 zg&dZ#8A=bktpsARst29tzFXK~)sv3yhG6toHdb^dd2piHcM3j_U-QjSVn=Fy?Kcw^ z`$cuaNq97EW*0iT1+wm}@F8fU48#t{O*VX572m{_mWzlUJRv>UK)>;vBpKz# zEW%=VV~i9Qdsn&6tBf(1iNO!P@d?7++SxgN3}}U4mL~mdv5_l9PT7{V+V!w;XWCPz z#cHg|j9r9n8sfNky6Adwjzm9wqLD(#IINC0UHr{0mpZ)TM?#Q!1zj!9gy1gyuW%jZ z!An?h;mYB2I;ERS#;m14H@I@}K(LT>a8~0In)>c5A7B5XMqXVF_OBZ0s`Zm(Ba~uR zXc!Av*l8hjeid~s#{imnMYA^Py$~RrS>Xq^9x^8sg>`<241n?Q-){sm7lT($&dGjt3VBzv!?NXd1~zaFHhyZwmLwnp1|m zE;O+X;|FV&TnQvjY|ouN+B2{ImULYX-o?p#5|w>w-7Kw>y!u{U`T$HHF*Ndl^0Uk; z)dVS0>wG!d>6ZvmUX)g59e@D&P3g)TlvoxGoY8V492iu-q$=-A!}i(Z=L+od=%6i? zcM;QIndieOB3&zL)5sXaSkoKSr1XFP$)%H1>E8~|$#GkK-@B+rCv=UjP!aCS?Rmm9 zs=_-ur1U)NU0~qZV2q4=@KS2vMZpMTa|=#i#pH}@tl#>I z^U!3(=ZA#0fL4CONCZ+LY_{AlLG7=xibOoVUz=m=HiwDcOPLZFQ8q=c{QdF!^6h>K zc!YmT-`BuBvBSdc7>$Q{G8c=4FDA>V6~8FqjTerjO;)6j)}H~rvB#ITrV_I2ijUGT zTKXeKS9Oo#=K}bwBNleHZFcVmZ3m17{M;oxU#AcDXIv=x5b=3DR5JijT=b-iy1roL z-gk(o$MgUo_Ls&_o}HjZ2UUIe3vj>zW8`$AVfdPpRv>PD_-t&-8CrNMs#m$eGb%*U znTI(2Y=y)bV&G`_EyGTuAB~~tGecq1D$xhY0VIXB$8H zIYJhtNngAviW#;-J_P0J3JFNg)LKYi&Kt2L6M$t%2ot-|`WMEgWsR1e{R%B-Heu; zI=O>&fyeNewtF9pXt0%obh6o*Kky==j4^_Pv$)M6xsqQON<7a2AxSx-t}(-G$t?Q@;|xOU z)Tz$i<8!P8m-i;%)8|xKa2o5WOjPc3Tq8)iye+E+-jvTz|m*q^aY<3B$MxMNuu z5X4$a+hK1UGq;nXdgCDPc!*`qZ?t?=viG!M4wU05gcySctSxqpN0hL9uq$NR!1YFd z+5(0Uv(t{6_T&C-m)Jlnd09@cBF_;)Itp%`><~7WOrxajaw$y-RTb2d=qUH^MxUK; zWA-sR*Cv=*0_O_|4^S#>6RKuEf_w_cdY!~nBpr+vC2js1^a(y~w&2BqRIm_6um_!a zIWm)5QuF@A<}{a7cFXyJYnM-XCg~@?5h`g^ja?F3^q{NdcxG#m>%#M{Y108#u^O*V`_{*+Z>DXT;co*q;$^Q9uEshG|zMVLnBaKSiAmSo$ zVQ32o1Dzl>QQ^iABY_A`oseH>kGn}PWo7l~EQYQZFV0zlSAULk?+MAcaRk`n`6x2- z(yJ|Fk7U`K|0v&{%!4j!8!I_wRSTn+(FIXy(|_ zh$ypDk1$JkCb{tw#r-BCUe@l07FYR1RZy&rL)sEqZ-eZLS$Q5AD8v`NXn&5r@P)@f z`*j0F@7Jm41@V_H$RNKdi7pw=ucf4lzM&^-(GE+!{9YMk;>DspU{kGub>bN%UR(Ra7m|u%6>%I7q<{a7~h%2z1k%u zPPz6I;r1yUehj^WIOEttYc!vb08?n4!Ov6WmfA2;lmb(YWCs2rV)xC>sfTv%zX<)% zkg~33R&TwP>C?Z-AzevHog?y z7{vM@t%u{>!6NEm>oX9HpC#NY3SV5+AnIlNJVl=MY@kR;ljgbR(h1ESKA{xIAt~pL z;uR(A%A(;}q38nm4O+x$8y;sxf)Zh5(&fHm!xrjo*C}Ulp@Fy{YC{i#A(}J(sp{of z%6q;{Knu>$u^&&HNCH@0V%p-QOfRTUi%^wqjY`9NpV5kK(S(mIzVl>Y0#F@ux7XEz z*K~*<%U4MY291d1hq8e4vwdooi8c|qt(3}GWH-NmBe5T&O5~1be&0R#N~2NyBV3$> zrG`0DddbXr?FWpwJG(H@mLU{e4)Hh#@ps5czJm5PE^T9g9W1>Y?E=53MRS2#xb1!{ ze=w3RDX}u3=Xm8og!$;wu&e^N#vUF?4^1|p_r0E;!18tZZL5Wt^}bWoR8@D)1c%;d z=skP!aG41W7wK2&xMjn}ZD|95MwQ0(vCshQiO4Pf!o(ppG>txFAs);xCLI$0KEbEpKl@2}aUAjIQLr^`X<4RS?H zf9K;3_K8CzpbJpFj|rGY5GT3FG&eMpwj$&&o9BBQTE{@^+DnwG3L*V=W?L6(*#dOw zqdbBmh+TjgTBo4eBXOzW!=@hVYybe_r3;fzNiU*14pPhOeQ_kc-h(NpAVDHlZy4Zr%zcmwrCpFJO1)r>9EhLly%6PaK%hJSKOH< z!=h+U9J8+%xco})2I^|tzzkJ%ripXmhnh~e2QN$DW=%@!Z6*(~&n;K3{PtsnK5`){tWH?Q#Zpd0o@m+Kwg%fQMbtR+={RGX;mr=<3hMZMQ0>dY-*;&<~& zYkmT2tWC1GZFtO|9V9J4ND6g(xIgHvpY+bgG~h_2PegYl$UsDdXycv@I+V?rdg$`a438)XExuM>uE zG*KNg3Oj5fRb@a6;YVJf zip%{AtP=RuM_%^_apz7sLQ7s!EKt!9oe~an%chm01(Mq=k9qs^imD9@qfn$|_vI+O ze+}yC>Gcm$?euKrpVKLYaR2>X8MU8 zS>4FP%#?IF#jvZ$qx?!Y&w}KuCv*0I zR*NanFE?onYYEoq?DP0QloZHOwz*|)*X{H*;dXsXnSdyIn_N%(Y<-<@VJK-GkQQ37 zHWTZ^9eFa2h>0I+I$T`t(n2}m4XRAdeJW6xG zTDD3yYB$R_(H)eh&cM~6!-gIl`sP4;NOo>6z}ExoHY?>A(eJj`FoiFXf{s`Z2}v&$ zFcHv0vKXDsz{3GnjDK>3bv9ZnJef>cyv~Bw2f8FQPi3{oXjC#JvnR@wZ>lt!-MLp$c+~`+Zk6*FIPIBSci4Yvo3t*_>aD${6?lOxC`(IAGefmT?s9KO^0jl(?*zhK)aD=Qm4fbCFQ7oaPTVZ&V zq;`Pi21Rj~dFi?HtzD163tnnAB)(*=^zIZh@6N{Mwm%kI0>DI6ZhZuVA(N|uK)|oV z;dwW7Ps*cyzG07#(rq%-iU5)xV=L(4Eh!pbVG5|Sz}lM9Lqr50jI1?%4IeBc@!r3?N{4tqBFfamrOjWt$VK??mvWe>*s3N5y(x-26_@}ZDzu0Rp71}DriKp>Bv=UMT=cN=Z)~nn+0G*B)v_?ZG>q^hef4-f2xd+If9PLlSBCJ&I+H z6FD}KEw^iSr-luE=e3LUzlTmg=hhV~e1b7tA86U@J};%`;33n*cFsi8{%%Ndu5J$H z#M8zvvqg}@&mST6Ax>P{GokvHMps=XtQy`{Je$zKNgAEBi&*v;10|zi`8P*w^0^q< zB@U#iPD&*NP?6+Na$uL|8+SP zeOA}Sa=sGi=X-L3Q{cTT=iArQ##?*fVP77TLjH#`c@dK`Q*a=WXUa7wE$a@hzgpEe z3qv(G7kx(f&Wb+qT-+9>_X($ftBqy2D}|#?*K=3M6vgoix4gj7Xd92b-}C^m7kLMQ z8>1byxPy(!@e`g@$u(;(b%!d1e`&v&jF)$)0kn3Lfdw))c5JkTtN2mt&+8$YZeF^$ zNE60heOBm)Cgb>VT|{RE5iyVyTCBp9`uDx12LgUrk{jN42G-&>7+2CV?Z(;SN&t=p z)=k=c-Eo?v^H_8lmd%0B4olJl!8;q($e?U?3)Fln71WmmHD!~6ES4m>mMmp5J4?J8 zlc7$K{Dot`GxsLE>tF~AGjLE}sv`%!qM8FyRm_8y)X+(dO9umib(6fd9wgi`gVcJXB_DF__`e+7_gJ{9zR@O6d$?4W9bAS@lVFpzbj9y*3q?$8gn4Pf}yui zD~&~=q5Ym_p?9HlOZ#((f$hx%$rWKx5a~LcH$z%$E2Rf;f&)~p z2zxB(i^R{CWl^%!2DwG(Ma&ygLzb`B8jiydw3q7n1^ym(AqpM9cw&uE+Yw=}2`bkw zLh&}Hsuc_}OrO>6?*$(B{2Px-ZLiNvh1tb(MW!>*MiBTjzS-`WhFL4D?JssK8~XS0 z5fiQnlMG2o%2CIrKE{tX7;i-HY%E!%B%|}vl{m-bGL?sDvp{3~_REzk-}n_YMCHV| zTB=KRgr=D%j)~De_fo?2v5j? za{)hb=IN9cNeWtjYhx-GSH^+xE+s2!k(&qBc_g%tj2_8kCn zr?2xT`l@ri4n-{BrDpdavT65yBr(cow%4w=gCo2G>g~x`T*=n zM+rkyXInj$n44MU*h1ueFu_Din0Ko>_8>XU=sj*pWrio6k>tm?=&&|g7%?ezxp=#( z?YpA6<8DWCt`J#=_TJ!4*}&j5C9~Z*BE;I=DV=@+8ywaUO-^C{HD2a>FC(g&h=2jQ zA-rBg=%~=$Rhgviu~)%I=Y$kS%IJLMY_5$tKk0gef6X#yYr+&dCH0if(uqDNw5w-V zL;;wA&!sJ8YeL!O&NN=HfoX2comVc3Lzphs`m2gMx_g?kaSoEM$%C=^3yqYgO^k|<@ezNv(8+F_2W|6MVjQ0gS7KwF;RYN15b0jnJV1F((l})+( zVV#@FDX3xN%uYJ_Omq4<=AF6*^k5R+ZU0!^!)y|FvZr*U{WkwI`Yjz(ZadqrwZXk z)R+=|gOR1%a{%GSXXg{D)DFP9!yArmt~h~Gw6bUPPjkAstl*uFt9DD{U;H>1rcLjJa?})@;6~-uL(b~ezjL;E}xs*A@$l#{zK7L$AS+$W|v!!Vh&AG z@4!Ca7RH*MWQ~0LqJ%k9nuw|x=f-2?A36%I-}LA; zP?#eXc=V;ILz?nMFv>6zbY^`V&A;KJYb@X@%JdvZF|k7rFIaPe1G`R; zv3T?}oULo7=^akt^wU?h$r9#k&F*h1jDarQ!Th8?8cwGQJ*T`eoyJ=m+PTz-M_zby zjqh-_SPgD^M!xnzcn21Ul&sC}FVF32IYYmtW6Y`XruT3G#=jI+%)MhH zrC$b^MI948#>L%m%`zlHsvLdev`EPO0Oc3g*5A;iE~nWMMG+lS%GI(A=|T zx~$eq>qC$^URSaN68)D(E=Bf-8sKQK5wU35udvryKmtcde~7Oj-#<|@Vx9X2%9ra{ z@V7?P53YI$EHWv2zD|EUD*wh4;|ji?I6%aJbgGc%I3ADmqw=pd+Czg%eW4f%hD7=K z`x|s1{xr0ucV*qPK+Zh-34fHCqxK2bgO`C{)ksJ7!9oZ~D==AJ?S#|Q99M;z^x~`C z9tg(+cdpLir1q`L;>(R~Bu#-Ca`TfQb^2;+IHwrLX^diMomEg zF6N;TbMXNx?N??Td4PBlcCwxs<&&?`AHrW*0AqXe7ix-mbH5Zr1UG$2&#x)i$s!k@ ze6{S&SL47|vyn{zGdk3e16&KCsnm=hOJI(mm!J~VLU2Pn>Gb3RR3W7bG}@{A9u1rm z(h1iz6$o$B1y`|uj`99^Pqk`oo2rEr>1J~!H3=lJy6=W9K^=s`yz+;WxZ^2Gpktp` zmq7f&M({A#-2TjRw~?tXs;yoM=s*E4V}OJc0%hmKjmZ`ZkhdYDMq#E0Ufcw!=1Sw* z$j-6=W3pp5)GcZZ;x%u3*SdT#jIHPTa6gyn_bi+rlphw`bzxhC&hFCJ;IKH86;6m_Va0k= zi>wURhpFieq|*Th{vIL%H%Ynl8pIRNhC7)(Vt?}^B~daM$F5L0>TONPMM4qhnKV0s zNIO3xz|&rkpIc?6IcQZXc;bX0g`&NArLg`@t8If=KonpUtz^(`f;c1j39_f3+9BST z!=*vqX|&*mhFkdW37-ddw7CMQuV{0TN6_&BCgREqg_}_Nw*kU(XdFVLfiKj^dQYLE zlICY(#J*X3{k51I2@d`{ak2x-{;fr$P+*d9n#i96H+ZoyEgMa@aI7KgVh@8~`WG^5 z07+TYV2ccyxl9D0&Ssg50bNuTTe)4+RRYF_yn=Qi;S~_E5A3UB%&k;Cjc62}%35f9 zs%48>?>pxFJWWdut3}S*8ZC>!hdc+6HQzuayep`e?1b{qAEJO54zNz-aI^A6Szrmn zrvsW&6k-GxaK~=PI<0IeH{e6X+&is(lQF2|bqcbQw2<^MuR||36%H8CJNMx_hx{zF!hH)<>p6nb65G7PFO5^SqW!oh* za0y>Orql;t=Dzpt*u}yj@f6E`o)WY!kHO^9vtid^{_ zpsqz)AH`^)ZID`-=kyMdTN2lWzbo#}mTh3LHbVMH1xKQ6PFwYqSP7)SiR&iXaBU@% zIY!iFbBFG#{<=@N8oI1<5SfBPMOp-Vz?i-(4~8(ZJq;$?m&Dj;iN7N-9GFrQRMj67 z4^DKA&0K`PH2ce2NtlPO8-!`94OIzEcqI;!_MC7Og#NsyeB4*xb^hPKx zdj9SiSbX1}k8+P8sIJr9l}RJQ6zM6s`SAlN{gqY+eYU@y%l27+SYq633L8T%lw{77 z28+x@ncDLI2{k&(#caSm#Lb}|bO!(Eo*X{%*~c*P2cYapI0mjDc5C(E0x7(63+{CDCQ;!v^M%2epc8eNrIWr5V$VK*|F7g^4D28jV*d8vUI>$miIkK^yLj722 za?#3#0p!*c@omrrM8Snz4&j(|(8}Hr$r87p72*2JUqP;)72R6egi#5soI8I$Jaznd zIKzsDCh>S~iS8c-&Lhl==Rpx7J@Vy8`WnBeh(+d|n$@_0b7=UQ$a)|I;%ExS!fus% z%rH?HzGK&Tc<%#GhQH2KK9m3eIN(V{K~#D2d>GnxDONQUkKwY8QeT*(=nSN(A6=C< zj%7VEjseN~kW3O4p?|2Vkdy5sYx}<1a# z*A%9Y5Rhgh_>GDat#J{3*(Foq#vAsAweu@cSL@f>`N!sH@~KGEKCnnp28l+Jl1{?K z-q}E+hcn9E)}>T6O*$K6{gE zPHo>Fa>Il>5zVnVDEDm9L(*n5IXFZ@?-QTSBl28RWhz{v9z*2c^t>y=hd+5NY(w|B zIwOWr;ano0NGn7n=Si?4FOiNUZa7XZmQx+Qzin`>XkYQi&QTKGDZUDKl*^W?@#iT#D7C4d-)BraW zV)*j6|Kwx%xad2cTBd{Hrl=ku^|$+yodm;ucvUJqbL==37*zf`4iCDEhp33=hhB z7cmSNqm^uz0Do`L{PWz&qXbPx@ztk7ndcf7(5)~hx^yX27s%^Uht)yy*igKubq%6( za6W&i(io&OUp`zy7H1bWirsd2?v?0@bE^>L8^Bi3G8bOL!Qb56Clkf2k&=qJD!QYn zKox9ow$&brXVnhY2XnHKjUEOJL4{_+Qr^rUp<>Kn7NLnVo_qphKtRCRewWaxR}6l?0w!f{X$I#B&}+jtu21INPi59|p)e(>6mTRBhT?0sQ| z;_t-^i`6D3Upd zQ$-JVSr279e;2l|oAN}32=pvEeq@Xw0*KY6EAt?(6glTq*2XNEuy{@ z0-533SuFevlC#Mi(|8=JoDR!$R(U#Wu|Rl@F#xRN*$9GD?l;kgG_ZygFh``oQjFdcU=O9~#G= z;1RDoQT!pcHMCkHlclVN#6X#s2+9!=i-q4#ojd!IN||{?o>c2AO~6;;%dw)a0|#sE zvNIl|YZTk*^X})3O{>`NiZa(1@d2qswIq70#*&#f1Xo*fl|FhANzsH0PSD7_L35Z2 z?FR}J?CB;cX7lJiIk->)uyCcE=J?A)toldXX9>%x%3?ECgDd==a4B}}$J7D2QiAWiduwyr zeI!yJ{X9Iho$rWH;1sRkh!xcy^Wm?(O++iC$2A}dt_N>2e9#EoBl@CbVm|za(vL{= zsEw<5PSR$Ab2yJ1q0R`(tgiMaMyJxO8H`2`H;swBA`nZBmFB7 zOSzJTcKE>=+#K*ivFw<1N;>C^9i-)GKdAL7xkd!_~-=eJ97x-Ys)3=UxHOBo(?4X;;h0oa&rVL+C- zt`ibFFf|Z9^u^O*`#txDAO4=}!}G4X3O{!}OtVht)TwhZVIULPU!kk?Xrb5e+T%mIj-u!xG)tB#nJi{l@veYb`6jTpXwBGq@oq zOC(b)8q1{i$Hp>?5M#FK_0wT5sXGi8HRkXlhAF~{x)5Rz+?W5KLQ=4C-6yr})tA10(qjQz-6cl`uw zEZtJsFEH=d#oDAOB8)T@vDT6Rx-AD4M*wvfvK3cm@_>$&ni1H@llB+>LSeQm*6vp63j0YI5-$UziVQMiG#i=6Kj9a|#S%`X$L1lV4kA@C=9&`?v6*!E;*if1KJFn|SY zTM5lOIj%A=N@^4&fdr*O2B`UnJSYVul2m}pGUkCX6rVaZ9a1oB4sKT@E1t{vrw6XQ z89PObpBMqgTqdd$V+0yuiJdS-`q67Qr_Z@$2;xQb*LZyW;26w+sK;14eg0%P^p4*P zH}5|XZlqjjAMayW{)T4TjJb~+TSU6j-IG$&cWg}l&608fgG-eS9F_BGZLguQJ-{UGzjEESQ4MyZO)ZnBDW~Y zT|=y%6Oj@~!l?|{`6#-bq5zP4IZqL9+&Z?DU~ENs*j-O)x>Bc)u`+2;R4K=Ocf4W_lVj-9oScd#anZ z38?F8OZ3{v^k*aR%yBvxtLcGt6yYUw^&-Izq(GS3yE)uL0RHe(cZVN%<;~%S-NRwD zw;Z+*92pw77>w42wp`-c^a6=GUg4@>5IH~~B1`U#`jtUPpD~JLnT!1DEWN^QKW3E*dJcR_pO38Y-Dv)0NG`kBZ>T>*)*q;g5fu$U%7F%{Sh7 zdAT%5S#%o$A%M@$GiQ&5MV4Y3m9bu0BTK(b-Djzclj;%>DDylc%VLHIN5UuJQ;)M6 z@5F_$je4l+3hQ=>0-dJ>=J+!waG4qyE)yUajsv@1!*xqQ6kw{U5sio*jX8C&A=C{E zyN&UtJ~I#^ViN;QPV+d$pW{miAR1|2l+E9aX`7@sDG&oH+(*kK#)3ge1}=1|N%WT? z2GivO#H{f$8JqqrMiE?JCGPyLKfXU?r|5d;@#MW-Nm1Zo9(xZQjCE>hf*~oQcb2~+ zV?uFkIE*mo&AYb8cEt;nK6x#R3#xc2t01_F;gutcVuV-dM@}{FC3feGeaSG9V5vX#U+&sXV9Cf7Jli?ienQRSg?0UyUIXpJE z7LIdnehXC>Cm+X1z!+g7b>h-%HOgGkW9qHwYqQ_h2QhHr1^F-cIu^NE?o$t&m`|*E zf*$y%YF5>4L=qpsdE{h`Hj!=#3*Fq%pB%^0eRO=2ESa36TBuOiP{P(InpkL25RVI` z-#kohml3pu*;7Q@@Cq=v1~yGn=QU1+>C@pp3d?VK0ZUagAUF2P&|4VBI_Ck^8IVqf zM*@k)BJ*0X3I-89Osx38H8&uh+u`gJk0JgER`6M3_PI`L9CMnS?ondtSp$kuJGl1MYG}&*Pautf+{b2NJ{*n__l@bA1;> zs)xkg(h_A1ay3%6w*LmqT@WHcU^?g*jFu6p#>(O`D`x8VFB3b0nS z61j@p7@39Zqg5Sw(y^G;N-S?D<0e7GY6<|CJmLn?jmgmx(71OQ)*Y?StcG((PK8H4 z|44Z3&p#h7eEw*dJ~|y%STV88{xnq}6^z0y|HCa|=e2tgsg-c-+-i8+dp{bk-p?a} z25}`BkxgfOHu1#7_ONHqjc_$qxQn(ZYcfdGMgpwjCZ2K@xWYy%VU+#FeIM_P zr8}9?k+FMU|AsgJrXctwaI0nG@o4eDva0aG{l&^l>!gQ#-u>eI1x)y**ef}mV!;(u z58gPzQ$V!hL(3J6Xu3xx)3Av|;LX!vnl?!-Dlp0{QOpq9)LSDHBj>8gU{pK&EVao>$slmZ-Qm{ga^w!UZP&(V5;0K)=F zj`QWSwubzzissz^r3dPxk`nteb;8c{}IHHhEKDrKDN( zj~28*2kcN5;s(H5pblfkZMABWoMzgY1q==iC&VS3FXFoBogV(2dfi}y0Okh}jBklP z@feXN!nADN4x2_{~&3OyKV2@z>h6h0z8F6Dv5%0+qH1Jd5_LzM_`(e0z7)Re z=I;(qa?CQtjYEidv>Cm=AYB$1jL@#whBJ@N&d|Ju_0FU(ZQB$H8Ndv&JcQ)R((av8 zA9hr9FO0y7ZW7LXmwu!EB30HL>19KB?CF{C+MBLm^#uKv(Ibi%F`~<({_&lYWAfiB$JogZWGBtR zK{m)ujHtvy7Mbc|4YY&+5}6{hy|QpmL_UTOgqFd|?1ByZfRr`^lsy<~>$^a5HLT6d zg*IM52R+;e8OU|4V01IfQ45v9u=PcgVQ|+J;xQA>(^B~<9$>lS(h-P^F{A4(zt!#- zBo1Ivp(`MZMC*JkKwJe$)~Ly1JG;(yaFzD&*%L~y{8wb!X2K#*8rP^?T|A4Z9*-*T z@V$Q%R!e(A;2|f-XeL7b+s0hg+3-CFc7!*+=9S?rCBlF9g-1X%jKMz&F1n>cHeGF2 z|MZP-Iy8Im;KA$%KJWns$aMU3aT7LhO^F9!g&)cETNEQ2k)R}shNERD+bE3BAkf%S zEm|U5C2&4up9*$pRKPrJl*K}RHXM`rzx?!kSVTnbm~wRl0Re6;f^0!-M?4=~p}{3` zO>_u2gYKICx=o5}#hF{oq_EGfJ#HFe-rEOaiB!2)S16Y6z%{xex*3X2k6F{8fe_O? z!Y&H}Cs^B*p@|vG;^TcXSkr|9wcXTnWo%*wFzkG1x$kinnL`U~JCK;VG^3<|hEEmrZ3QHmQr5ycXfhM8`vFhRz zQP`(ADiFR?)Q$`l!Zd-9WyF}ZTxAm&7LFmt;=`v<>@MIIUs}cbt5#t~WGev__Yu(? zaBfAI>IvpiwLYxc6$YB)^;5ub^-@S7iwP3V@fY<;LoP~5_ zL0yk{xMi~S`ROowsvgcBI~hG0y+mN$fnamZTL07c-S@@MI_VF%p6KHCTm*+3K=?z@ z#z?vM_F}ep3jn7n9XvxhZ5v~$&E$*dEBisU^ecg`@!L8aP%7Y8%Dn*9q&o14#)jPF zwuEj8T~_{q_yPxtz#f+2AjV1)(Qj+wJi1tB)&Dfk-LdTfTU_Db2aqX3F`W`bMI@W> zW}nxSv)P9~5r0>iSi9{eXIqv@n1fU%FaC+RC;_leEpmYWiSH5wp?#ET!W;-JAW=FH z1r`Abc4FmR#?)s837}$IXTZK65_{#py{xgc5oWs}v-@ z=(6462VeR^q~6(p%SvBj?qPnI$QsY53iB{fe5xE{RjN5t+gDhUY+}d4i!}_cdGghE z>6IqFO1Wgn*C?fN{_Koz4j9L2L$ z9wz?v+l)oa1wQ4Z$^dM)o1p;QT4o1>oQ$02jjWCYe7qPoKgCEGfNOvNpCN4HFjvMK z^3%uG+AbhQ#H!Y0#*;)A+!`w)BBJ?)cbs(81LI&k3DP_i0%SHxXw}sV01@giyCZQK zJW);fid_XT$n+A7v!@Zymcnu;F2ER67f{g@kKrRK2=P&?MmvbPd8$1zvc|`z!tA+o zVG*Jj8QFxp1LBbu8hXPJ1F4!Ym}ZeS%Sz3?xa>5IWlcA$C~ZtMSn^xtc^5diPAC$p z<8ASv?Npxv2ds1iU0C9~B~bRkAP!+6dxpaIFP^^t!4Gj_AUhaw=w~-(@KswMF4kot z{@nT10gMu2rhU07hs=oaX~fsAJ^PUNPz?qQ!Y^{q8c1D3&lm(p2uE&UlnGL}o+Yp` z>J{%8u@jMOaNo7TBP<5DrIC4KeZo3axrSS1a-)gJvg2M9wfwbBQnb?mx?1}idd`N;&+W9xn1P?RMTjPfncLv7U@d_Rzji_BH{&)MS&WU)@AQv#o-{u zD;jPt6HSv^wDZm?Y&ytdyd|Pfsu!O<(L>+5B7(p}sH3H_?A`9$CS%hj`*I}iCo7t#Utkc`C+7_audOv&Aa+2Y^<*9V`h7ch#iBv%R#r=30mBM)BvX?)uJf`~s)6ffv?^Ad0@E5( zTSMNj5CfmZ=$a*1gMUvYJJwgX^|{mc{>@J_Ogh3$JKV3@zDh)I{BrPM$Q(Y*xccjF z%a#kD>QeATe6v5XV=`sHetwbu58F25oKxa5Z<#2TEU3`-vEOtV?!W>E>FBzPI@sly zx;he=Mv+{l__U|9NH@DuA@(|oXg_y;77oG5VxNW=t<2-1G5dI^e-+~rQ(QH`o>;QN zrA1gwqd_>HuLUiJM7Esk?M1GUFUj0Ocz%h3#<}aV#8$;{vU=0eT8Zg9;)QwT!qmi8 zitOe{gO1153?|pBr1nl7eIgvV{EG1W%XWs3JbH>=Cdb1d3&$rA**=E13$ZlFPv7^* zTz4TIVb9e_2 z9t3BHze+s2|LY=H+ zzM;w>4V$69xEZ@y4h!+IIffY#e9+V!97w_si`encx3rdd{&ah;#dH&5;69!)KcMp3Cfp#OfObI`fFcu z1%_`z(BlfKC=Hy&4sK}MYViKE=9%DGNj$Zds0P0~ZdJsUpHbx2`fjK7w2qS0y z;f#dTfyniewl1TWjAsL^*CNc5P9#+Sv z&r<5E@19~g4v82lFf9CEqa*FyBDGZ@U}D?W*zLf_{+&8=GMuABZGl!2Emi}T(Dl(h zgj*p6<1CUq35g{JQ zm6^j5`xJ@lYsl|kC z45u|>`D;iPZWdL524F-7Ll2m~k?Gu4>~Jh-K!&l7oE8Dc{-%uHnT9l=gfuqR-;+XSjFM0M3-95 z5q5JSHqRN3gEaSAr>;oL_%zX(@4e~raNG4b{yrccJ!G0u?lA3`-qzoW$j88GHE7rkIn5Smyv7?VNj$|HG7=v7@FGt40xeE(nj)M6X z1$#75AcJHx_0@2SMcO6{M4&{tu}RSCSI6O&;3tL2=;`72SfJc$!ghd2SSft9~ z;yCtL2EvM0>#cOi4L10^1y{2BgtZ0;tp9(_om*_%_jSjAyh)TuQKCfMD2bLO*EhxU z94Bp-ZG}AS#ch$a80MD&8MYY?wqaYbVGqMzGBkS_Hf-oifCX6>VC#lDL$W2^+QMz@ zD2`*hmTfh*Y{jx})Ws4-QoKl{BwqIU{wO+`JLh7@N&bLDiK2MwH2t4e|`MTpMXb-3~_PwDV_a-Cq<`cRb}Qu+6v`~IN< zmT;x2-#$OG9O-SHZwdyUoSBXl0|6fzu;f?;RZ*F#u;k`uW+m~A2^=J4i_|th24Csu zS06Kd9RUb;eKxiYuio)mKxtUaS$T zqS+7|Ab;;nPMKFv{HqCdZo^ruHxo0rObD``2Pf+K=v6%EU9c@YW_VUJmFQ_ndJl#L z0^)T!%@%O6pF=2`%nPr^n(4GkYgo-uku0BG(E`q+-4yai7_vjM9a16-Q9PEN%)I_# zs<|rZ4;8Q;h^j6P4i37{ymj&7uH8M6mX@Z2X*zY**7)3ui`XGlNDZiyX6aSvH&TUe z90~+TTfoO2>op%pFcaYEENKy(~nJr*jH(w@+5FumaCN zHo2(agAHhLVAa^VsPj=*_6;&In}idPU#tgf`CIr&E>^>V_gS@LjW~4j*5zqus6G(K zNi7`I+?2Muc;g5Y&5>kcK^zK^!VOytE*`fS6@n=}M$2ltum)hL@7n{6FP;8XVI zZCk5-$|V8vV)keG1^UV6ygAj?%l^*XJpNisg5>;aG<9_AZ|^;*`ry0VZoS-H{(8sV zI>lSTbocIo@CbR_=5&e~WtnyIkpCBr50+$3p-G?uZ5Jy&iI^o}hX2XA|ZxUU=E;?QEuEAY&OL+L)Fy1Ja}+SeX@? zzX3-Q?wjr%uvLTj6$n(oN-Kl`FNm2R-=cdX79lId<8W6l`?xZ>qM{hjaattp&&}rM_@fTxd*0gC z!|!(UkyLc`pQom#u*b#&gL=i|&A*OI^E(LHvq@@+-G(25%A=4gNWD*ABy94WCKpXf zdb)b~ncQX~&Vo@f!AZllG6`bI3)3-kYUTz5FXjOY%CyM`gT*_ogLIE#vl~P|dj-N{ zk7XOJJw|=mQ(#q|)ERNE;z>4<5+b@4Htm7B>h%Ob#Tw1=AD=U0vkPntx4DJ$rw}BA z&0WSOtC%j0?XLHcFo1*H+UYSNI@_sYUv^TJh)Rmou?YlOD*jJWz_tntGtS@+2`i(^hY9D9OIqqlE;vYf_AvuTW53SGV z#y+auwNJG5?PCd8|ATN#0|TaJV!RaX?rFN<^ZR~_IM$iRXkk;Ft!+&vO#Sd0-e`(Y zMxJJtqV1{a3FD{TQjiw;8K^8Gzb!-LUQNuI-j*=7)@K$d_c+HqgmDIGG{7TOP~d(*F!QMH=qBf#IrU?U+I+j*1x=B!XZjS zz`N@~%uo{8Jjz5-y#gPUx0O2eBg9%VJ?aYYZ;+or84c_YwTr3&mMx)mYEjKeK;pA+ zj8n;8j5*Lwkzt?>fCzf$&;fOkM}* zm6>rkzLuZQ4q4$2-^RMO9{u4rA6vkBO2~gKjj4$(t&Mf|G*5J+P@DQQu8s8VH3jC0SRzEm96+qZ+0UouHDF5!+Y)4BZ%AV$k( zYHo?vy*b-PQB)w?Ly4tOxTNB7mA2&N?Xn5Hw$2G73Qnzdieh;JpsSJt_3Sl2xp*BF zJw{(IoG6}Cs!*p%r$_2nDpT6Q1ESnb(&$jWMzwwgj$w zfLa*#4f>1*VDmkdomqL=aG(s~w;u6vuX_^k){7Je2aWsOnbf83-j>;5Q{V}pHYys+ zMR-Y0KQ>l0pA`}lwJiE7-laku**umV5CCtJO%T9Yk>~3tepZq@#lno=xJlcl9RbE8*zHAO(q$SLB>GPBW=IY#a%DZoFeRCNW%%mOA`8%d|Ol6aAt)@s^sy3c~_5-#@RXl8Qx zMZ+4KpWd5r_00DpV51m$5XbASH!lu{yMvRclyA`6%1QmtW>YZeiqpDKU_p{_La5I6 z?R^wSH`+K^WIhelkYgrbL#r1C<4$p^^9`Xj?PKw@@ahmGf$60cyUQvn zIz<7ak~#FOwVHg(=89h}P-l&Vd^0t}5{`u{i5f((8#y=4#uEb)4aGBTlK42Q-yXCtP&(vO{>W>BsVMSV)?1;bmB!G;5Jhq-J#;~KX=`ifc0#0 zyaw52r-$N~+PVVg*Yg`k+S}VOX-*DgfmQ;54%oZ~KzV@~vYSOtmZ}+oWM%}g+c=R= zA5SbpEzMf*INX6<1~1ODsOw~H8VH3jz-E!6^6P|M%!r}bJOi5|gu{b3PESnR5f{(8 zlH--_W-(dNXkyL$!%HW~Sm9OUAW6WQ%VcY6c@3o@#c@`9B++;^u?>(SM=Fui&Xjn35VahZ~gaqEV$b3rCQXYbuCLPG*RP3hQvjiuol-*&?!q(B{ zUfEw%M!8&r$q-lQYpc`AmyJ&DeF~Q+zrPc#C(xynJTNd&Gcr6jw(r2MAMlQUg8`Jx zEB3A@<0;B&Y8}B~GYrav7=M z(4*5cFem7;M@?^@1-vpbZGJX320Y0T0-xFP&Ur^s>Ra1gh#?vbmv}ltoU8LuNR%HnTEhZSH-jW%~kK z-|qVaSD%a`wxS@-(?b_#{XXY+aaewp(;HzlUNHnnKwDoxDHlT{=Sf7s^yhQda`GBk zbXzrtLm~ldJe__|5vLYFgw&i&?FVDbOWHHAAJYVYfHNi=?g2r%N%iB+XcT&BnciG* zi+K%sv=$A;PK#ePa&y6)IDgev2B_Fu!nfHQR#m50Css*y30jVqOrTe%m+}+dB41*F zR8*~E@_N*=F!kw)8)%MJ3#crBWYyDc&W-bBO#siv8{q|{h7}qqtgF2476uNU&%1Ow z=t*8W&!Z0IX0w%0=pG%asx01r`-uryPr3l4DIs6FG`2D~n|R@`pZ&|8zW%<0Y+gq) zxx7(VUk3*rHXu7Z026{)O?1;E9HJs`3C=ORd(^WS{2)<0pSZ@VC0-(&YQhAF+G*|{-0iE5NLi;WcgiStE}1oT*E{^#6PbNR-M1t`*B)wUHr$pfiL zzFYpaHlKK0ZW@+*8G|Q6oLV({LDl3~so-6GF!^UT87dvHMngbK(Buv ztRUzM*WR`ND@RAVP_eY7!JsB}Zk3FvL$Gq7?{Y~P$5s(XlYxBmC8H=pPUuD*&KFMf6+XxH~&{mDiApYP$(?k8W|m(AuJ zr2gZWU3SuuCdf>RTuI7AjHc#a+90 zGqSGPs`L!GXPurnP~oe?S0RG31FPaJjy4#J z40A?JYeqwy8lmOoWhReqBlOxvc}UJIM(41*98ZLnBphl0*>aK5xgMP|O|&c2Twx91 z3eepymSB7ic2#CTK!Fn+xlk`-FEIne_vl=dUD#4SWUUTt~v@g3xB>gyZZak z9>~QlH9%&j~(w?c!d@Y6(A z4>isTw>Bsxkq_k~b*K(`>WZ_57oFqFL2pJIZLg*a`}-sAuN)e5<`zoFAKvlMUJ?f1 z-dgboqLS~F=7T)Jw_i(P6u&s+fDOHVJ~2NV|BuF|+S4FgJJ)in5r6<^4aH2!RGg$A zbT@h{h|#z}CA^R&FKQ4@1%+N&imXF)o?C=D;xipIH%a1o%TrPVPGPqhz=hV9R`Bo+ zVptldvPtCmCOGx#sN=(4tbsoHskPl8gQQa%fQdp^R!oIP@}M!1Ts@_v0c^373|`tA zHi5L|z|iZ#mFw9|8c^aVl&b^fazW8?QWgV=zf8=}%t%|T=L$6VXlAo{mfYrV|LEk| zzoqe7YU|YM#A;0*`1Pp>xcVvNWvl56F|+fHlV@*EjL-j5O5j;@F1+PDJumz1XxXb*-@z8Qf+&RmM&g`uh~+p*o$X=UG>oHs<)79t15;SqcCK|7$z@VIoxY)s_}Od z_tA)~6l`fPFqT$%QWpe65(J>*F4qv-<)qWEj5M8aaFz&(3W<28oJ_`C{X6=d z^cq1J*iM|EW-9PAX(+0|m;x#yIBN~4VmV~uRVk4^e)9&-QUfSiiyiE^L?;`qw=!M? zre=OF3P!$2Es)ZMbpJu90yy^dGA)KqI)W`t$)_6BVpmT{99(y}cQ@X2A848@2_xk{ zW3j{wzxgMBf2R6bG-Cbfr=P$5*{b({nzx_&3BL2gNo{kV;~3ud@bE%;dU7ecG@tsf z_SU)=seOI7wKY_W;KDMIne>_q)#|_zE7R1oNXo8+6Gr_Ng=+M%rd-7#BuwDFNCgUO zEMHoH2}7fhF1hoku|Ge%C#=$wt)M?!YrY&`6`tHem~O? z+|F9ZhO4c)p3Ju=ms;I8$?5+aD7t@i_;;WEx0inSb`;Iw2!|Iuuf2Am{Kz9uJdn`m z&Ncj?-#_yceCL~!jh1>o^}afrFMe^?_W9VYUr!}B4tICAJqFLQGlU%$7?P5cg_i}) z$=a5Sd-m=r>%NAD21iRvv%~NAInJGb+p&A+ULxNTj@vEkdDGXC`ku8iW&wt-vz*GL zagbI_dN%F|1sYxaoi>_I(;N1i_Qp-q#Vn@UQq4j-wRASp+x3Iy#;&))%Sn$ zb0xSb&Psl-%y>O5SrBo0?} zWQ_0&?id(k!R3s0(up_hFaI>+GjDBgY8me0)GE{bP!8BVRcH-|7Rg(!+;?ta-Z~v^0UKtz=y{v t^co!)WZec!I8S;%w>Mw)4S$|){}*=FCxWMII6nXY002ovPDHLkV1i1Y;I04w diff --git a/site/themes/pinniped/static/img/nigel-brown.png b/site/themes/pinniped/static/img/nigel-brown.png new file mode 100644 index 0000000000000000000000000000000000000000..63124eb95130b0a883428c865e44354648d660c6 GIT binary patch literal 9225 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