ContainerImage.Pinniped/test/integration/supervisor_oidcclientsecret...

1044 lines
34 KiB
Go

// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"go.pinniped.dev/generated/latest/apis/supervisor/clientsecret/v1alpha1"
supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/oidcclientsecretstorage"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/test/testlib"
)
func TestKubectlOIDCClientSecretRequest_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
tests := []struct {
name string
oicClientSecretRequestYAML func(string) string
cmdInvocation func(string) []string
assertOnStdOut func(t *testing.T, oidcClientName string, stdOutString string)
assertOnStdErr func(t *testing.T, oidcClientName, tempFileName, stdErrString string)
wantErr string
}{
{
name: "kubectl create oidcclientsecretrequest file will return a simple success status message",
oicClientSecretRequestYAML: func(name string) string {
return here.Docf(`
apiVersion: clientsecret.supervisor.%s/v1alpha1
kind: OIDCClientSecretRequest
metadata:
name: %s
namespace: %s
spec:
generateNewSecret: true
revokeOldSecrets: false
`, env.APIGroupSuffix, name, env.SupervisorNamespace)
},
cmdInvocation: func(filePath string) []string {
return []string{"create", "-f", filePath}
},
assertOnStdOut: func(t *testing.T, oidcClientName string, stdOutString string) {
require.Equal(t, fmt.Sprintf("oidcclientsecretrequest.clientsecret.supervisor.%s/%s created\n", env.APIGroupSuffix, oidcClientName), stdOutString)
},
assertOnStdErr: func(t *testing.T, oidcClientName, tempFileName, stdErrString string) {
requireCleanKubectlStderr(t, stdErrString)
},
},
{
name: "kubectl apply an oidcclientsecretrequest file will return a simple success status message",
oicClientSecretRequestYAML: func(name string) string {
return here.Docf(`
apiVersion: clientsecret.supervisor.%s/v1alpha1
kind: OIDCClientSecretRequest
metadata:
name: %s
namespace: %s
spec:
generateNewSecret: true
revokeOldSecrets: false
`, env.APIGroupSuffix, name, env.SupervisorNamespace)
},
cmdInvocation: func(filePath string) []string {
return []string{"apply", "-f", filePath}
},
assertOnStdOut: func(t *testing.T, oidcClientName string, stdOutString string) {
require.Equal(t, fmt.Sprintf("oidcclientsecretrequest.clientsecret.supervisor.%s/%s created\n", env.APIGroupSuffix, oidcClientName), stdOutString)
},
assertOnStdErr: func(t *testing.T, oidcClientName, tempFileName, stdErrString string) {
requireCleanKubectlStderr(t, stdErrString)
},
},
{
name: "kubectl create an oidcclientsecretrequest -o yaml will return a yaml doc with the correct structure",
oicClientSecretRequestYAML: func(name string) string {
return here.Docf(`
apiVersion: clientsecret.supervisor.%s/v1alpha1
kind: OIDCClientSecretRequest
metadata:
name: %s
namespace: %s
spec:
generateNewSecret: true
revokeOldSecrets: false
`, env.APIGroupSuffix, name, env.SupervisorNamespace)
},
cmdInvocation: func(filePath string) []string {
return []string{"create", "-f", filePath, "-o", "yaml"}
},
assertOnStdOut: func(t *testing.T, oidcClientName string, stdOutString string) {
var yamlObj map[string]interface{}
err := yaml.Unmarshal([]byte(stdOutString), &yamlObj)
require.NoError(t, err)
require.Lenf(t, yamlObj, 5, "yaml object should have 5 top level keys (apiVersion, kind, metadata, spec, status): %v", yamlObj)
require.Equal(t, yamlObj["apiVersion"], fmt.Sprintf("clientsecret.supervisor.%s/v1alpha1", env.APIGroupSuffix))
require.Equal(t, yamlObj["kind"], "OIDCClientSecretRequest")
metadataMap, ok := yamlObj["metadata"].(map[string]interface{})
require.True(t, ok, "metadata should be a map")
require.Len(t, metadataMap, 3, "metadata should contain only 3 keys (creationTimestamp, name, namespace): %v", metadataMap)
require.Equal(t, metadataMap["name"], oidcClientName)
require.Equal(t, metadataMap["namespace"], env.SupervisorNamespace)
timestamp, ok := metadataMap["creationTimestamp"].(string)
require.Truef(t, ok, "timestamp should be a string: %v", timestamp)
parsedTime, err := time.Parse(time.RFC3339, timestamp)
require.NoError(t, err)
testutil.RequireTimeInDelta(t, parsedTime, time.Now(), 1*time.Minute)
specMap, ok := yamlObj["spec"].(map[string]interface{})
require.True(t, ok, "spec should be a map")
require.Len(t, specMap, 2, "spec should contain only 2 keys (generateNewSecret, revokeOldSecrets): %v", specMap)
require.Equal(t, specMap["generateNewSecret"], true)
require.Equal(t, specMap["revokeOldSecrets"], false)
statusMap, ok := yamlObj["status"].(map[string]interface{})
require.True(t, ok, "status should be a map")
require.Len(t, specMap, 2, "status should contain only 2 keys (generatedSecret, totalClientSecrets): %v", statusMap)
require.Regexp(t, "^[0-9a-z]{64}$", statusMap["generatedSecret"], "generated secret must be precisely 40 hex encoded characters")
require.Equal(t, statusMap["totalClientSecrets"], float64(1))
},
assertOnStdErr: func(t *testing.T, oidcClientName, tempFileName, stdErrString string) {
requireCleanKubectlStderr(t, stdErrString)
},
},
{
name: "kubectl get oidcclientsecretrequest should return an empty list",
oicClientSecretRequestYAML: func(s string) string {
return ``
},
cmdInvocation: func(filePath string) []string {
return []string{"get", "oidcclientsecretrequest", "-n", env.SupervisorNamespace}
},
assertOnStdOut: func(t *testing.T, oidcClientName string, stdOutString string) {
require.Empty(t, stdOutString)
},
assertOnStdErr: func(t *testing.T, oidcClientName, tempFileName, stdErrString string) {
require.Contains(t, stdErrString, fmt.Sprintf("No resources found in %s namespace.", env.SupervisorNamespace))
},
},
{
name: "kubectl delete oidcclientsecretrequest will return a not found error",
oicClientSecretRequestYAML: func(name string) string {
return here.Docf(`
apiVersion: clientsecret.supervisor.%s/v1alpha1
kind: OIDCClientSecretRequest
metadata:
name: %s
namespace: %s
`, env.APIGroupSuffix, name, env.SupervisorNamespace)
},
cmdInvocation: func(filePath string) []string {
return []string{"delete", "-f", filePath}
},
assertOnStdOut: func(t *testing.T, oidcClientName string, stdOutString string) {
require.Empty(t, stdOutString)
},
assertOnStdErr: func(t *testing.T, oidcClientName, tempFileName, stdErrString string) {
require.Contains(t, stdErrString, fmt.Sprintf("Error from server (NotFound): error when deleting \"%s\": the server could not find the requested resource\n", tempFileName))
},
wantErr: `exit status 1`,
},
{
name: "kubectl create oidcclientsecretrequest with bad data (incorrect namespace: concierge instead of supervisor) will return an error with a reasonable formatting",
oicClientSecretRequestYAML: func(name string) string {
return here.Docf(`
apiVersion: clientsecret.supervisor.%s/v1alpha1
kind: OIDCClientSecretRequest
metadata:
name: %s
namespace: %s
spec:
generateNewSecret: true
revokeOldSecrets: false
`, env.APIGroupSuffix, name, env.ConciergeNamespace)
},
cmdInvocation: func(filePath string) []string {
return []string{"create", "-f", filePath, "-o", "yaml"}
},
assertOnStdOut: func(t *testing.T, oidcClientName string, stdOutString string) {
require.Equal(t, "", stdOutString)
},
assertOnStdErr: func(t *testing.T, oidcClientName, tempFileName, stdErrString string) {
require.Contains(t, stdErrString, fmt.Sprintf(
`Error from server (BadRequest): error when creating "%s": namespace must be %s on OIDCClientSecretRequest, was %s`,
tempFileName, env.SupervisorNamespace, env.ConciergeNamespace),
)
},
wantErr: `exit status 1`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
supervisorClient := testlib.NewSupervisorClientset(t)
oidcClient, err := supervisorClient.ConfigV1alpha1().OIDCClients(env.SupervisorNamespace).Create(ctx,
&supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "client.oauth.pinniped.dev-",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"https://example.com",
"http://127.0.0.1/yoyo",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:token-exchange",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"openid",
"offline_access",
"username",
"groups",
"pinniped:request-audience",
},
},
},
metav1.CreateOptions{},
)
t.Cleanup(func() {
err := supervisorClient.ConfigV1alpha1().OIDCClients(env.SupervisorNamespace).Delete(ctx, oidcClient.Name, metav1.DeleteOptions{})
require.NoError(t, err)
})
require.NoError(t, err)
clientSecretRequestYAML := tt.oicClientSecretRequestYAML(oidcClient.Name)
secretReqFile := testutil.WriteStringToTempFile(t, "clientsecretrequest-*.yaml", clientSecretRequestYAML)
//nolint:gosec // not worried about these potentially tainted inputs
cmd := exec.CommandContext(ctx, "kubectl", tt.cmdInvocation(secretReqFile.Name())...)
var stdOut, stdErr bytes.Buffer
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err = cmd.Run()
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
tt.assertOnStdOut(t, oidcClient.Name, stdOut.String())
tt.assertOnStdErr(t, oidcClient.Name, secretReqFile.Name(), stdErr.String())
})
}
}
func TestCreateOIDCClientSecretRequest_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
type testRequest struct {
secretRequest *v1alpha1.OIDCClientSecretRequest
wantSecretCount int
wantErr func(string) string
}
type StoredClientSecret struct {
SecretHashes []string `json:"hashes"`
Version string `json:"version"`
}
tests := []struct {
name string
clientSecretRequests func(name string) []testRequest
}{
{
name: "create 1st client secret",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "create 2 client secrets, count increases to 2",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
RevokeOldSecrets: false,
},
},
wantSecretCount: 2,
},
}
},
},
{
name: "create 2nd client secret, revoke original client secret, storage secret should count remains 1",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
RevokeOldSecrets: true,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "when no secret exists, revoking without generating results in noop without errors",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 0,
},
}
},
},
{
name: "when no secret exists, not generating and not revoking results in noop without errors",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: false,
},
},
wantSecretCount: 0,
},
}
},
},
{
name: "having created a first secret, do not generate a new secret and do not revoke old secrets results in noop without errors",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: false,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "having created a first secret, on a 2nd request do not create a new secret but also revoke old secrets, result is existing secret remains (safety net, disallow deletion of final secret)",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "having created a first secret, on a 2nd request that creates a new secret and also revoke old secrets, result is a single new client secret",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
RevokeOldSecrets: true,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "having created a first client secret, on subsequent secret requests (up to 5) stored secret hashes should increase, " +
"and when a sixth secret request is made which revokes all old secrets, the result will be one final new secret hash",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 2,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 3,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 4,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 5,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
RevokeOldSecrets: true,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "having created a first client secret, on subsequent secret requests (up to 5) stored secret hashes should increase, " +
"and when a sixth secret request is made without creating a new client secret but revoking all old secrets, the result " +
"will be that the last created secret will remain (safety net)",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 2,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 3,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 4,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 5,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 1,
},
}
},
},
{
name: "having already created 5 client secrets, generating a 5th secret should error when revokeOldSecrets is false",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 1,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 2,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 3,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 4,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 5,
},
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: env.SupervisorNamespace,
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
RevokeOldSecrets: false,
},
},
wantSecretCount: 5,
wantErr: func(name string) string {
return fmt.Sprintf("OIDCClient %s has too many secrets, spec.revokeOldSecrets must be true", name)
},
},
}
},
},
{
name: "generateName is unsupported on OIDCClientSecretRequest objects",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "some-generate-name-prefix-",
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 0,
wantErr: func(name string) string {
return fmt.Sprintf(
`OIDCClientSecretRequest.clientsecret.supervisor.%s "" is invalid: [metadata.generateName: Invalid value: "some-generate-name-prefix-": generateName is not supported, metadata.name: Required value: name or generateName is required]`,
env.APIGroupSuffix)
},
},
}
},
},
{
name: "name must not equal client.oauth.pinniped.dev-",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-",
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 0,
wantErr: func(name string) string {
return fmt.Sprintf(
`OIDCClientSecretRequest.clientsecret.supervisor.%s "client.oauth.pinniped.dev-" is invalid: metadata.name: Invalid value: "client.oauth.pinniped.dev-": must not equal 'client.oauth.pinniped.dev-'`,
env.APIGroupSuffix)
},
},
}
},
},
{
name: "name must contain client.oauth.pinniped.dev-",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "doesnt-contain-prefix",
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 0,
wantErr: func(name string) string {
return fmt.Sprintf(
`OIDCClientSecretRequest.clientsecret.supervisor.%s "doesnt-contain-prefix" is invalid: metadata.name: Invalid value: "doesnt-contain-prefix": must start with 'client.oauth.pinniped.dev-'`,
env.APIGroupSuffix)
},
},
}
},
},
{
name: "namespace on the OIDCClientSecretRequest object does not match the namespace on the associated OIDCClient",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "some-other-namespace",
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
},
wantSecretCount: 0,
wantErr: func(name string) string {
return `the namespace of the provided object does not match the namespace sent on the request`
},
},
}
},
},
{
name: "client secret request created for an oidc client that does not exist should error",
clientSecretRequests: func(name string) []testRequest {
return []testRequest{
{
secretRequest: &v1alpha1.OIDCClientSecretRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "client.oauth.pinniped.dev-client-that-does-not-exist",
},
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: false,
RevokeOldSecrets: true,
},
},
wantSecretCount: 0,
wantErr: func(name string) string {
return fmt.Sprintf(
`OIDCClientSecretRequest.clientsecret.supervisor.%s "client.oauth.pinniped.dev-client-that-does-not-exist" is invalid: metadata.name: Not found: "client.oauth.pinniped.dev-client-that-does-not-exist"`,
env.APIGroupSuffix)
},
},
}
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
kubeClient := testlib.NewKubernetesClientset(t)
supervisorClient := testlib.NewSupervisorClientset(t)
oidcClient, err := supervisorClient.ConfigV1alpha1().OIDCClients(env.SupervisorNamespace).Create(ctx,
&supervisorconfigv1alpha1.OIDCClient{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "client.oauth.pinniped.dev-",
},
Spec: supervisorconfigv1alpha1.OIDCClientSpec{
AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{
"https://example.com",
"http://127.0.0.1/yoyo",
},
AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:token-exchange",
},
AllowedScopes: []supervisorconfigv1alpha1.Scope{
"openid",
"offline_access",
"username",
"groups",
"pinniped:request-audience",
},
},
},
metav1.CreateOptions{},
)
require.NoError(t, err)
t.Cleanup(func() {
deleteErr := supervisorClient.ConfigV1alpha1().
OIDCClients(env.SupervisorNamespace).Delete(ctx, oidcClient.Name, metav1.DeleteOptions{})
require.NoError(t, deleteErr)
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
_, err := kubeClient.CoreV1().Secrets(oidcClient.Namespace).
Get(ctx, oidcclientsecretstorage.New(nil).GetName(oidcClient.UID), metav1.GetOptions{})
requireEventually.Error(err, "deleting OIDCClient should result in deleting storage secrets")
requireEventually.True(k8serrors.IsNotFound(err),
"deleting OIDCClient should result in deleting storage secrets")
}, 2*time.Minute, 250*time.Millisecond)
})
cacheOfGeneratedSecrets := []string{}
hasSecretBeenGenerated := false
for n, ttt := range tt.clientSecretRequests(oidcClient.Name) {
clientSecretRequestResponse, err := supervisorClient.ClientsecretV1alpha1().
OIDCClientSecretRequests(env.SupervisorNamespace).Create(ctx, ttt.secretRequest, metav1.CreateOptions{})
if ttt.wantErr != nil { //nolint:nestif
require.EqualError(t, err, ttt.wantErr(oidcClient.Name))
} else {
require.NoError(t, err)
require.Equal(t, ttt.secretRequest.ObjectMeta.Name, clientSecretRequestResponse.ObjectMeta.Name,
"name in response should match name in sent request")
require.Equal(t, ttt.secretRequest.ObjectMeta.Namespace, clientSecretRequestResponse.ObjectMeta.Namespace,
"namespace in response should match namespace in sent request")
testutil.RequireTimeInDelta(t, clientSecretRequestResponse.ObjectMeta.CreationTimestamp.Time, time.Now(), 1*time.Minute)
require.Equalf(t, ttt.secretRequest.TypeMeta, clientSecretRequestResponse.TypeMeta,
"type meta of response should match the sent request")
require.Equalf(t, ttt.secretRequest.Spec, clientSecretRequestResponse.Spec,
"spec of response should match the sent request")
require.Equalf(t, clientSecretRequestResponse.Status.TotalClientSecrets, ttt.wantSecretCount,
"expected secret count is incorrect on iteration %d", n)
if ttt.secretRequest.Spec.GenerateNewSecret {
require.Len(t, clientSecretRequestResponse.Status.GeneratedSecret, hex.EncodedLen(32),
"generated secret is not a hex encoded string")
} else {
require.Empty(t, clientSecretRequestResponse.Status.GeneratedSecret,
"when GenerateSecret is false no secret should be generated")
}
// api will not let you revoke your last secret unless you are also generating a new secret
if ttt.secretRequest.Spec.RevokeOldSecrets {
if ttt.secretRequest.Spec.GenerateNewSecret {
// we will add the newly generated secret below
cacheOfGeneratedSecrets = []string{}
} else {
// if we aren't creating a new secret, we need to keep the most recent secret
cacheOfGeneratedSecrets = retainOnlyMostRecentSecret(cacheOfGeneratedSecrets)
}
}
if ttt.secretRequest.Spec.GenerateNewSecret {
cacheOfGeneratedSecrets = prependSecret(cacheOfGeneratedSecrets, clientSecretRequestResponse.Status.GeneratedSecret)
hasSecretBeenGenerated = true
}
require.Len(t, cacheOfGeneratedSecrets, ttt.wantSecretCount,
"number of generated secrets should match number of hashed secrets")
}
// even if we got an error, we want to get the storage secret and make assertions about its state
storageSecret, getStorageSecretError := kubeClient.CoreV1().Secrets(oidcClient.Namespace).
Get(ctx, oidcclientsecretstorage.New(nil).GetName(oidcClient.UID), metav1.GetOptions{})
if !hasSecretBeenGenerated {
require.Error(t, getStorageSecretError, "expected not found error")
require.True(t, k8serrors.IsNotFound(getStorageSecretError), "expected not found error")
// no storage secret was created, so no reason to continue making assertions
continue
}
require.NoError(t, getStorageSecretError)
storedClientSecret := StoredClientSecret{}
err = json.Unmarshal(storageSecret.Data["pinniped-storage-data"], &storedClientSecret)
require.NoError(t, err)
require.Len(t, storedClientSecret.SecretHashes, ttt.wantSecretCount)
for i, storedSecretHash := range storedClientSecret.SecretHashes {
require.NoErrorf(t, bcrypt.CompareHashAndPassword([]byte(storedSecretHash), []byte(cacheOfGeneratedSecrets[i])),
"hash %q at index %d is not the hash of secret %q at (%s)", storedSecretHash, i, cacheOfGeneratedSecrets[i])
}
}
})
}
}
func retainOnlyMostRecentSecret(list []string) []string {
if len(list) == 0 {
return []string{}
}
return []string{list[0]}
}
func prependSecret(list []string, newItem string) []string {
newList := make([]string, 0, len(list)+1)
newList = append(newList, newItem)
newList = append(newList, list...)
return newList
}
func TestOIDCClientSecretRequestUnauthenticated_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
t.Cleanup(cancel)
client := testlib.NewAnonymousSupervisorClientset(t)
_, err := client.ClientsecretV1alpha1().OIDCClientSecretRequests(env.SupervisorNamespace).Create(ctx,
&v1alpha1.OIDCClientSecretRequest{
Spec: v1alpha1.OIDCClientSecretRequestSpec{
GenerateNewSecret: true,
},
}, metav1.CreateOptions{})
require.Error(t, err)
if env.KubernetesDistribution == testlib.AKSDistro {
// On AKS the error just says "Unauthorized".
require.Contains(t, err.Error(), "Unauthorized")
} else {
// Clusters which allow anonymous auth will give a more detailed error.
require.Contains(t, err.Error(), `User "system:anonymous" cannot create resource "oidcclientsecretrequests"`)
}
}