Merge pull request #1466 from vmware-tanzu/get_kubeconfig_discover_username_group_scopes

`pinniped get kubeconfig` discovers support for username/groups scopes
This commit is contained in:
Joshua Casey 2023-03-30 14:47:56 -05:00 committed by GitHub
commit 7d394658cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 474 additions and 153 deletions

View File

@ -23,6 +23,7 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/utils/strings/slices"
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
@ -97,6 +98,11 @@ type getKubeconfigParams struct {
installHint string installHint string
} }
type discoveryResponseScopesSupported struct {
// Same as ScopesSupported in the Supervisor's discovery handler's struct.
ScopesSupported []string `json:"scopes_supported"`
}
func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
var ( var (
cmd = &cobra.Command{ cmd = &cobra.Command{
@ -232,11 +238,9 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
cluster.CertificateAuthorityData = flags.concierge.caBundle cluster.CertificateAuthorityData = flags.concierge.caBundle
} }
// If there is an issuer, and if any upstream IDP flags are not already set, then try to discover Supervisor upstream IDP details. if len(flags.oidc.issuer) > 0 {
// When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input. Maybe they know something err = pinnipedSupervisorDiscovery(ctx, &flags, deps.log)
// that we can't know, like the name of an IDP that they are going to define in the future. if err != nil {
if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "") {
if err := discoverSupervisorUpstreamIDP(ctx, &flags, deps.log); err != nil {
return err return err
} }
} }
@ -733,21 +737,75 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool
return false return false
} }
func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams, log plog.MinLogger) error { func pinnipedSupervisorDiscovery(ctx context.Context, flags *getKubeconfigParams, log plog.MinLogger) error {
httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle) // Make a client suitable for calling the provider, which may or may not be a Pinniped Supervisor.
oidcProviderHTTPClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle)
if err != nil { if err != nil {
return err return err
} }
pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(ctx, flags.oidc.issuer, httpClient) // Call the provider's discovery endpoint, but don't parse the results yet.
discoveredProvider, err := discoverOIDCProvider(ctx, flags.oidc.issuer, oidcProviderHTTPClient)
if err != nil {
return err
}
// Parse the discovery response to find the Supervisor IDP discovery endpoint.
pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(discoveredProvider)
if err != nil { if err != nil {
return err return err
} }
if pinnipedIDPsEndpoint == "" { if pinnipedIDPsEndpoint == "" {
// The issuer is not advertising itself as a Pinniped Supervisor which supports upstream IDP discovery. // The issuer is not advertising itself as a Pinniped Supervisor which supports upstream IDP discovery.
// Since this field is not present, then assume that the provider is not a Pinniped Supervisor. This field
// was added to the discovery response in v0.9.0, which is so long ago that we can assume there are no such
// old Supervisors in the wild which need to work with this CLI command anymore. Since the issuer is not a
// Supervisor, then there is no need to do the rest of the Supervisor-specific business logic below related
// to username/groups scopes or IDP types/names/flows.
return nil return nil
} }
// Now that we know that the provider is a Supervisor, perform an additional check based on its response.
// The username and groups scopes were added to the Supervisor in v0.20.0, and were also added to the
// "scopes_supported" field in the discovery response in that same version. If this CLI command is talking
// to an older Supervisor, then remove the username and groups scopes from the list of requested scopes
// since they will certainly cause an error from the old Supervisor during authentication.
supervisorSupportsBothUsernameAndGroupsScopes, err := discoverScopesSupportedIncludesBothUsernameAndGroups(discoveredProvider)
if err != nil {
return err
}
if !supervisorSupportsBothUsernameAndGroupsScopes {
flags.oidc.scopes = slices.Filter(nil, flags.oidc.scopes, func(scope string) bool {
if scope == oidcapi.ScopeUsername || scope == oidcapi.ScopeGroups {
log.Info("removed scope from --oidc-scopes list because it is not supported by this Supervisor", "scope", scope)
return false // Remove username and groups scopes if there were present in the flags.
}
return true // Keep any other scopes in the flag list.
})
}
// If any upstream IDP flags are not already set, then try to discover Supervisor upstream IDP details.
// 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 flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "" {
if err := discoverSupervisorUpstreamIDP(ctx, pinnipedIDPsEndpoint, oidcProviderHTTPClient, flags, log); err != nil {
return err
}
}
return nil
}
func discoverOIDCProvider(ctx context.Context, issuer string, httpClient *http.Client) (*coreosoidc.Provider, error) {
discoveredProvider, err := coreosoidc.NewProvider(coreosoidc.ClientContext(ctx, httpClient), issuer)
if err != nil {
return nil, fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
}
return discoveredProvider, nil
}
func discoverSupervisorUpstreamIDP(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client, flags *getKubeconfigParams, log plog.MinLogger) error {
discoveredUpstreamIDPs, err := discoverAllAvailableSupervisorUpstreamIDPs(ctx, pinnipedIDPsEndpoint, httpClient) discoveredUpstreamIDPs, err := discoverAllAvailableSupervisorUpstreamIDPs(ctx, pinnipedIDPsEndpoint, httpClient)
if err != nil { if err != nil {
return err return err
@ -787,21 +845,24 @@ func newDiscoveryHTTPClient(caBundleFlag caBundleFlag) (*http.Client, error) {
return phttp.Default(rootCAs), nil return phttp.Default(rootCAs), nil
} }
func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) { func discoverIDPsDiscoveryEndpointURL(discoveredProvider *coreosoidc.Provider) (string, error) {
discoveredProvider, err := coreosoidc.NewProvider(coreosoidc.ClientContext(ctx, httpClient), issuer)
if err != nil {
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
}
var body idpdiscoveryv1alpha1.OIDCDiscoveryResponse var body idpdiscoveryv1alpha1.OIDCDiscoveryResponse
err = discoveredProvider.Claims(&body) err := discoveredProvider.Claims(&body)
if err != nil { if err != nil {
return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err) return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
} }
return body.SupervisorDiscovery.PinnipedIDPsEndpoint, nil return body.SupervisorDiscovery.PinnipedIDPsEndpoint, nil
} }
func discoverScopesSupportedIncludesBothUsernameAndGroups(discoveredProvider *coreosoidc.Provider) (bool, error) {
var body discoveryResponseScopesSupported
err := discoveredProvider.Claims(&body)
if err != nil {
return false, fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err)
}
return slices.Contains(body.ScopesSupported, oidcapi.ScopeUsername) && slices.Contains(body.ScopesSupported, oidcapi.ScopeGroups), nil
}
func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client) ([]idpdiscoveryv1alpha1.PinnipedIDP, error) { func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client) ([]idpdiscoveryv1alpha1.PinnipedIDP, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, pinnipedIDPsEndpoint, nil) request, err := http.NewRequestWithContext(ctx, http.MethodGet, pinnipedIDPsEndpoint, nil)
if err != nil { if err != nil {

View File

@ -81,6 +81,7 @@ func TestGetKubeconfig(t *testing.T) {
"discovery.supervisor.pinniped.dev/v1alpha1": { "discovery.supervisor.pinniped.dev/v1alpha1": {
"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers" "pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"
}, },
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"],
"another-key": "another-value" "another-key": "another-value"
}`, issuerURL, issuerURL) }`, issuerURL, issuerURL)
} }
@ -1086,7 +1087,8 @@ func TestGetKubeconfig(t *testing.T) {
"issuer": "%s", "issuer": "%s",
"discovery.supervisor.pinniped.dev/v1alpha1": { "discovery.supervisor.pinniped.dev/v1alpha1": {
"pinniped_identity_providers_endpoint": "https%%://illegal_url" "pinniped_identity_providers_endpoint": "https%%://illegal_url"
} },
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience", "username", "groups"]
}`, issuerURL) }`, issuerURL)
}, },
wantLogs: func(issuerCABundle string, issuerURL string) []string { wantLogs: func(issuerCABundle string, issuerURL string) []string {
@ -1369,7 +1371,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -1406,41 +1408,41 @@ func TestGetKubeconfig(t *testing.T) {
}, },
wantStdout: func(issuerCABundle string, issuerURL string) string { wantStdout: func(issuerCABundle string, issuerURL string) string {
return here.Doc(` return here.Doc(`
apiVersion: v1 apiVersion: v1
clusters: clusters:
- cluster: - cluster:
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
server: https://fake-server-url-value server: https://fake-server-url-value
name: kind-cluster-pinniped name: kind-cluster-pinniped
contexts: contexts:
- context: - context:
cluster: kind-cluster-pinniped cluster: kind-cluster-pinniped
user: kind-user-pinniped user: kind-user-pinniped
name: kind-context-pinniped name: kind-context-pinniped
current-context: kind-context-pinniped current-context: kind-context-pinniped
kind: Config kind: Config
preferences: {} preferences: {}
users: users:
- name: kind-user-pinniped - name: kind-user-pinniped
user: user:
exec: exec:
apiVersion: client.authentication.k8s.io/v1beta1 apiVersion: client.authentication.k8s.io/v1beta1
args: args:
- login - login
- static - static
- --enable-concierge - --enable-concierge
- --concierge-api-group-suffix=pinniped.dev - --concierge-api-group-suffix=pinniped.dev
- --concierge-authenticator-name=test-authenticator - --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=webhook - --concierge-authenticator-type=webhook
- --concierge-endpoint=https://fake-server-url-value - --concierge-endpoint=https://fake-server-url-value
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
- --token=test-token - --token=test-token
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`) `)
}, },
}, },
{ {
@ -1470,42 +1472,42 @@ func TestGetKubeconfig(t *testing.T) {
}, },
wantStdout: func(issuerCABundle string, issuerURL string) string { wantStdout: func(issuerCABundle string, issuerURL string) string {
return here.Doc(` return here.Doc(`
apiVersion: v1 apiVersion: v1
clusters: clusters:
- cluster: - cluster:
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
server: https://fake-server-url-value server: https://fake-server-url-value
name: kind-cluster-pinniped name: kind-cluster-pinniped
contexts: contexts:
- context: - context:
cluster: kind-cluster-pinniped cluster: kind-cluster-pinniped
user: kind-user-pinniped user: kind-user-pinniped
name: kind-context-pinniped name: kind-context-pinniped
current-context: kind-context-pinniped current-context: kind-context-pinniped
kind: Config kind: Config
preferences: {} preferences: {}
users: users:
- name: kind-user-pinniped - name: kind-user-pinniped
user: user:
exec: exec:
apiVersion: client.authentication.k8s.io/v1beta1 apiVersion: client.authentication.k8s.io/v1beta1
args: args:
- login - login
- static - static
- --enable-concierge - --enable-concierge
- --concierge-api-group-suffix=pinniped.dev - --concierge-api-group-suffix=pinniped.dev
- --concierge-authenticator-name=test-authenticator - --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=webhook - --concierge-authenticator-type=webhook
- --concierge-endpoint=https://fake-server-url-value - --concierge-endpoint=https://fake-server-url-value
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
- --credential-cache= - --credential-cache=
- --token-env=TEST_TOKEN - --token-env=TEST_TOKEN
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`) `)
}, },
}, },
{ {
@ -1573,7 +1575,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -1659,7 +1661,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
@ -1772,7 +1774,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
@ -1881,7 +1883,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -1960,7 +1962,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2039,7 +2041,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2114,7 +2116,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2187,7 +2189,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2267,7 +2269,272 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true
`,
issuerURL,
base64.StdEncoding.EncodeToString([]byte(issuerCABundle)))
},
},
{
name: "IDP discovery endpoint is listed in OIDC discovery document but scopes_supported does not include username or groups, so do not request username or groups in kubeconfig's --scopes",
args: func(issuerCABundle string, issuerURL string) []string {
return []string{
"--kubeconfig", "./testdata/kubeconfig.yaml",
"--skip-validation",
}
},
conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object {
return []runtime.Object{
credentialIssuer(),
jwtAuthenticator(issuerCABundle, issuerURL),
}
},
oidcDiscoveryResponse: func(issuerURL string) string {
return here.Docf(`{
"issuer": "%s",
"discovery.supervisor.pinniped.dev/v1alpha1": {
"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"
},
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience"]
}`, issuerURL, issuerURL)
},
idpsDiscoveryResponse: here.Docf(`{
"pinniped_identity_providers": [
{"name": "some-oidc-idp", "type": "oidc"}
]
}`),
wantLogs: func(issuerCABundle string, issuerURL string) []string {
return []string{
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL),
`"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`,
`"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`,
`"level"=0 "msg"="removed scope from --oidc-scopes list because it is not supported by this Supervisor" "scope"="username"`,
`"level"=0 "msg"="removed scope from --oidc-scopes list because it is not supported by this Supervisor" "scope"="groups"`,
}
},
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
- --enable-concierge
- --concierge-api-group-suffix=pinniped.dev
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=jwt
- --concierge-endpoint=https://fake-server-url-value
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
- --issuer=%s
- --client-id=pinniped-cli
- --scopes=offline_access,openid,pinniped:request-audience
- --ca-bundle-data=%s
- --request-audience=test-audience
- --upstream-identity-provider-name=some-oidc-idp
- --upstream-identity-provider-type=oidc
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)))
},
},
{
name: "IDP discovery endpoint is listed in OIDC discovery document but scopes_supported is not listed (which shouldn't really happen), so do not request username or groups in kubeconfig's --scopes",
args: func(issuerCABundle string, issuerURL string) []string {
return []string{
"--kubeconfig", "./testdata/kubeconfig.yaml",
"--skip-validation",
}
},
conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object {
return []runtime.Object{
credentialIssuer(),
jwtAuthenticator(issuerCABundle, issuerURL),
}
},
oidcDiscoveryResponse: func(issuerURL string) string {
return here.Docf(`{
"issuer": "%s",
"discovery.supervisor.pinniped.dev/v1alpha1": {
"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"
}
}`, issuerURL, issuerURL)
},
idpsDiscoveryResponse: here.Docf(`{
"pinniped_identity_providers": [
{"name": "some-oidc-idp", "type": "oidc"}
]
}`),
wantLogs: func(issuerCABundle string, issuerURL string) []string {
return []string{
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL),
`"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`,
`"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`,
`"level"=0 "msg"="removed scope from --oidc-scopes list because it is not supported by this Supervisor" "scope"="username"`,
`"level"=0 "msg"="removed scope from --oidc-scopes list because it is not supported by this Supervisor" "scope"="groups"`,
}
},
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
- --enable-concierge
- --concierge-api-group-suffix=pinniped.dev
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=jwt
- --concierge-endpoint=https://fake-server-url-value
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
- --issuer=%s
- --client-id=pinniped-cli
- --scopes=offline_access,openid,pinniped:request-audience
- --ca-bundle-data=%s
- --request-audience=test-audience
- --upstream-identity-provider-name=some-oidc-idp
- --upstream-identity-provider-type=oidc
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)))
},
},
{
name: "IDP discovery endpoint is listed in OIDC discovery document but scopes_supported does not include username or groups, and scopes username and groups were also not requested by flags",
args: func(issuerCABundle string, issuerURL string) []string {
return []string{
"--kubeconfig", "./testdata/kubeconfig.yaml",
"--skip-validation",
"--oidc-scopes", "foo,bar,baz",
}
},
conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object {
return []runtime.Object{
credentialIssuer(),
jwtAuthenticator(issuerCABundle, issuerURL),
}
},
oidcDiscoveryResponse: func(issuerURL string) string {
return here.Docf(`{
"issuer": "%s",
"discovery.supervisor.pinniped.dev/v1alpha1": {
"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"
},
"scopes_supported": ["openid", "offline_access", "pinniped:request-audience"]
}`, issuerURL, issuerURL)
},
idpsDiscoveryResponse: here.Docf(`{
"pinniped_identity_providers": [
{"name": "some-oidc-idp", "type": "oidc"}
]
}`),
wantLogs: func(issuerCABundle string, issuerURL string) []string {
return []string{
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
`"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`,
fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL),
`"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`,
`"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`,
}
},
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
- --enable-concierge
- --concierge-api-group-suffix=pinniped.dev
- --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=jwt
- --concierge-endpoint=https://fake-server-url-value
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
- --issuer=%s
- --client-id=pinniped-cli
- --scopes=foo,bar,baz
- --ca-bundle-data=%s
- --request-audience=test-audience
- --upstream-identity-provider-name=some-oidc-idp
- --upstream-identity-provider-type=oidc
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 provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2291,7 +2558,8 @@ func TestGetKubeconfig(t *testing.T) {
jwtAuthenticator(issuerCABundle, issuerURL), jwtAuthenticator(issuerCABundle, issuerURL),
} }
}, },
oidcDiscoveryStatusCode: http.StatusNotFound, // should not get called by the client in this case oidcDiscoveryResponse: happyOIDCDiscoveryResponse, // still called to check for support of username and groups scopes
idpsDiscoveryStatusCode: http.StatusNotFound, // should not get called by the client in this case
wantLogs: func(issuerCABundle string, issuerURL string) []string { wantLogs: func(issuerCABundle string, issuerURL string) []string {
return []string{ return []string{
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
@ -2345,7 +2613,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2428,7 +2696,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2486,7 +2754,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2547,7 +2815,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2608,7 +2876,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2670,7 +2938,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2733,7 +3001,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2794,7 +3062,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2854,7 +3122,7 @@ func TestGetKubeconfig(t *testing.T) {
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details for more details
provideClusterInfo: true provideClusterInfo: true
`, `,
issuerURL, issuerURL,
@ -2888,40 +3156,40 @@ func TestGetKubeconfig(t *testing.T) {
}, },
wantStdout: func(issuerCABundle string, issuerURL string) string { wantStdout: func(issuerCABundle string, issuerURL string) string {
return here.Doc(` return here.Doc(`
apiVersion: v1 apiVersion: v1
clusters: clusters:
- cluster: - cluster:
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
server: https://fake-server-url-value server: https://fake-server-url-value
name: kind-cluster-pinniped name: kind-cluster-pinniped
contexts: contexts:
- context: - context:
cluster: kind-cluster-pinniped cluster: kind-cluster-pinniped
user: kind-user-pinniped user: kind-user-pinniped
name: kind-context-pinniped name: kind-context-pinniped
current-context: kind-context-pinniped current-context: kind-context-pinniped
kind: Config kind: Config
preferences: {} preferences: {}
users: users:
- name: kind-user-pinniped - name: kind-user-pinniped
user: user:
exec: exec:
apiVersion: client.authentication.k8s.io/v1beta1 apiVersion: client.authentication.k8s.io/v1beta1
args: args:
- login - login
- static - static
- --enable-concierge - --enable-concierge
- --concierge-api-group-suffix=pinniped.dev - --concierge-api-group-suffix=pinniped.dev
- --concierge-authenticator-name=test-authenticator - --concierge-authenticator-name=test-authenticator
- --concierge-authenticator-type=webhook - --concierge-authenticator-type=webhook
- --concierge-endpoint=https://fake-server-url-value - --concierge-endpoint=https://fake-server-url-value
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
- --token=test-token - --token=test-token
command: '.../path/to/pinniped' command: '.../path/to/pinniped'
env: [] env: []
installHint: Test installHint message installHint: Test installHint message
provideClusterInfo: true provideClusterInfo: true
`) `)
}, },
}, },
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package oidcclient implements a CLI OIDC login flow. // Package oidcclient implements a CLI OIDC login flow.
@ -27,6 +27,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/term" "golang.org/x/term"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/strings/slices"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
@ -741,7 +742,7 @@ func (h *handlerState) initOIDCDiscovery() error {
if err := h.provider.Claims(&discoveryClaims); err != nil { if err := h.provider.Claims(&discoveryClaims); err != nil {
return fmt.Errorf("could not decode response_modes_supported in OIDC discovery from %q: %w", h.issuer, err) return fmt.Errorf("could not decode response_modes_supported in OIDC discovery from %q: %w", h.issuer, err)
} }
h.useFormPost = stringSliceContains(discoveryClaims.ResponseModesSupported, "form_post") h.useFormPost = slices.Contains(discoveryClaims.ResponseModesSupported, "form_post")
return nil return nil
} }
@ -756,15 +757,6 @@ func validateURLUsesHTTPS(uri string, uriName string) error {
return nil return nil
} }
func stringSliceContains(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidctypes.Token, error) { func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidctypes.Token, error) {
h.logger.V(plog.KlogLevelDebug).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. // Perform OIDC discovery. This may have already been performed if there was not a cached base token.