Use custom suffix in Spec.Authenticator.APIGroup of TokenCredentialRequest

When the Pinniped server has been installed with the `api_group_suffix`
option, for example using `mysuffix.com`, then clients who would like to
submit a `TokenCredentialRequest` to the server should set the
`Spec.Authenticator.APIGroup` field as `authentication.concierge.mysuffix.com`.

This makes more sense from the client's point of view than using the
default `authentication.concierge.pinniped.dev` because
`authentication.concierge.mysuffix.com` is the name of the API group
that they can observe their cluster and `authentication.concierge.pinniped.dev`
does not exist as an API group on their cluster.

This commit includes both the client and server-side changes to make
this work, as well as integration test updates.

Co-authored-by: Andrew Keesler <akeesler@vmware.com>
Co-authored-by: Ryan Richard <richardry@vmware.com>
Co-authored-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
Ryan Richard
2021-02-03 15:49:15 -08:00
co-authored by Andrew Keesler Margo Crawford
parent 26922307ad
commit 288d9c999e
11 changed files with 203 additions and 82 deletions
+18 -15
View File
@@ -12,7 +12,7 @@ import (
"net/url"
"strings"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/clientcmd"
@@ -34,11 +34,12 @@ type Option func(*Client) error
// Client is a configuration for talking to the Pinniped concierge.
type Client struct {
namespace string
authenticator *corev1.TypedLocalObjectReference
caBundle string
endpoint *url.URL
apiGroupSuffix string
namespace string
authenticatorName string
authenticatorKind string
caBundle string
endpoint *url.URL
apiGroupSuffix string
}
// WithNamespace configures the namespace where the TokenCredentialRequest is to be sent.
@@ -55,18 +56,15 @@ func WithAuthenticator(authType, authName string) Option {
if authName == "" {
return fmt.Errorf("authenticator name must not be empty")
}
authenticator := corev1.TypedLocalObjectReference{Name: authName}
c.authenticatorName = authName
switch strings.ToLower(authType) {
case "webhook":
authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
authenticator.Kind = "WebhookAuthenticator"
c.authenticatorKind = "WebhookAuthenticator"
case "jwt":
authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
authenticator.Kind = "JWTAuthenticator"
c.authenticatorKind = "JWTAuthenticator"
default:
return fmt.Errorf(`invalid authenticator type: %q, supported values are "webhook" and "jwt"`, authType)
}
c.authenticator = &authenticator
return nil
}
}
@@ -133,7 +131,7 @@ func New(opts ...Option) (*Client, error) {
return nil, err
}
}
if c.authenticator == nil {
if c.authenticatorName == "" {
return nil, fmt.Errorf("WithAuthenticator must be specified")
}
if c.endpoint == nil {
@@ -180,13 +178,18 @@ func (c *Client) ExchangeToken(ctx context.Context, token string) (*clientauthen
if err != nil {
return nil, err
}
replacedAPIGroupName, _ := groupsuffix.Replace(auth1alpha1.SchemeGroupVersion.Group, c.apiGroupSuffix)
resp, err := clientset.LoginV1alpha1().TokenCredentialRequests(c.namespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: c.namespace,
},
Spec: loginv1alpha1.TokenCredentialRequestSpec{
Token: token,
Authenticator: *c.authenticator,
Token: token,
Authenticator: v1.TypedLocalObjectReference{
APIGroup: &replacedAPIGroupName,
Kind: c.authenticatorKind,
Name: c.authenticatorName,
},
},
}, metav1.CreateOptions{})
if err != nil {
+76 -41
View File
@@ -21,6 +21,7 @@ import (
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/testutil"
)
@@ -220,47 +221,7 @@ func TestExchangeToken(t *testing.T) {
t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second))
// Start a test server that returns successfully and asserts various properties of the request.
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/apis/login.concierge.pinniped.dev/v1alpha1/namespaces/test-namespace/tokencredentialrequests", r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t,
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.pinniped.dev/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.pinniped.dev",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`,
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
caBundle, endpoint := runFakeServer(t, expires, "pinniped.dev")
client, err := New(WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
require.NoError(t, err)
@@ -279,4 +240,78 @@ func TestExchangeToken(t *testing.T) {
},
}, got)
})
t.Run("changing the API group suffix for the client sends the custom suffix on the CredentialRequest's APIGroup and on its spec.Authenticator.APIGroup", func(t *testing.T) {
t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second))
caBundle, endpoint := runFakeServer(t, expires, "suffix.com")
client, err := New(WithAPIGroupSuffix("suffix.com"), WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
require.NoError(t, err)
got, err := client.ExchangeToken(ctx, "test-token")
require.NoError(t, err)
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
ExpirationTimestamp: &expires,
},
}, got)
})
}
// Start a test server that returns successfully and asserts various properties of the request.
func runFakeServer(t *testing.T, expires metav1.Time, pinnipedAPIGroupSuffix string) (string, string) {
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t,
fmt.Sprintf("/apis/login.concierge.%s/v1alpha1/namespaces/test-namespace/tokencredentialrequests", pinnipedAPIGroupSuffix),
r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t, here.Docf(
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.%s/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.%s",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`, pinnipedAPIGroupSuffix, pinnipedAPIGroupSuffix),
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: fmt.Sprintf("login.concierge.%s/v1alpha1", pinnipedAPIGroupSuffix),
Kind: "TokenCredentialRequest",
},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
return caBundle, endpoint
}