WIP towards revoking upstream refresh tokens during GC

- Discover the revocation endpoint of the upstream provider in
  oidc_upstream_watcher.go and save it into the cache for future use
  by the garbage collector controller
- Adds RevokeRefreshToken to UpstreamOIDCIdentityProviderI
- Implements the production version of RevokeRefreshToken
- Implements test doubles for RevokeRefreshToken for future use in
  garbage collector's unit tests
- Prefactors the crud and session storage types for future use in the
  garbage collector controller
- See remaining TODOs in garbage_collector.go
This commit is contained in:
Ryan Richard
2021-10-22 14:32:26 -07:00
parent 303b1f07d3
commit d0ced1fd74
14 changed files with 988 additions and 65 deletions

View File

@@ -51,22 +51,20 @@ type JSON interface{} // document that we need valid JSON types
func New(resource string, secrets corev1client.SecretInterface, clock func() time.Time, lifetime time.Duration) Storage {
return &secretsStorage{
resource: resource,
secretType: corev1.SecretType(fmt.Sprintf(secretTypeFormat, resource)),
secretVersion: []byte(secretVersion),
secrets: secrets,
clock: clock,
lifetime: lifetime,
resource: resource,
secretType: secretType(resource),
secrets: secrets,
clock: clock,
lifetime: lifetime,
}
}
type secretsStorage struct {
resource string
secretType corev1.SecretType
secretVersion []byte
secrets corev1client.SecretInterface
clock func() time.Time
lifetime time.Duration
resource string
secretType corev1.SecretType
secrets corev1client.SecretInterface
clock func() time.Time
lifetime time.Duration
}
func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON, additionalLabels map[string]string) (string, error) {
@@ -86,28 +84,14 @@ func (s *secretsStorage) Get(ctx context.Context, signature string, data JSON) (
if err != nil {
return "", fmt.Errorf("failed to get %s for signature %s: %w", s.resource, signature, err)
}
if err := s.validateSecret(secret); err != nil {
return "", err
}
if err := json.Unmarshal(secret.Data[secretDataKey], data); err != nil {
return "", fmt.Errorf("failed to decode %s for signature %s: %w", s.resource, signature, err)
err = FromSecret(s.resource, secret, data)
if err != nil {
return "", fmt.Errorf("error during get for signature %s: %w", signature, err)
}
return secret.ResourceVersion, nil
}
func (s *secretsStorage) validateSecret(secret *corev1.Secret) error {
if secret.Type != s.secretType {
return fmt.Errorf("%w: %s must equal %s", ErrSecretTypeMismatch, secret.Type, s.secretType)
}
if labelResource := secret.Labels[SecretLabelKey]; labelResource != s.resource {
return fmt.Errorf("%w: %s must equal %s", ErrSecretLabelMismatch, labelResource, s.resource)
}
if !bytes.Equal(secret.Data[secretVersionKey], s.secretVersion) {
return ErrSecretVersionMismatch // TODO should this be fatal or not?
}
return nil
}
func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) {
// Note: There may be a small bug here in that toSecret will move the SecretLifetimeAnnotationKey date forward
// instead of keeping the storage resource's original SecretLifetimeAnnotationKey value. However, we only use
@@ -154,6 +138,36 @@ func (s *secretsStorage) DeleteByLabel(ctx context.Context, labelName string, la
return nil
}
// FromSecret is similar to Get, but for when you already have a Secret in hand, e.g. from an informer.
// It validates and unmarshals the Secret. The data parameter is filled in as the result.
func FromSecret(resource string, secret *corev1.Secret, data JSON) error {
if err := validateSecret(resource, secret); err != nil {
return err
}
if err := json.Unmarshal(secret.Data[secretDataKey], data); err != nil {
return fmt.Errorf("failed to decode %s: %w", resource, err)
}
return nil
}
func secretType(resource string) corev1.SecretType {
return corev1.SecretType(fmt.Sprintf(secretTypeFormat, resource))
}
func validateSecret(resource string, secret *corev1.Secret) error {
secretType := corev1.SecretType(fmt.Sprintf(secretTypeFormat, resource))
if secret.Type != secretType {
return fmt.Errorf("%w: %s must equal %s", ErrSecretTypeMismatch, secret.Type, secretType)
}
if labelResource := secret.Labels[SecretLabelKey]; labelResource != resource {
return fmt.Errorf("%w: %s must equal %s", ErrSecretLabelMismatch, labelResource, resource)
}
if !bytes.Equal(secret.Data[secretVersionKey], []byte(secretVersion)) {
return ErrSecretVersionMismatch // TODO should this be fatal or not?
}
return nil
}
//nolint: gochecknoglobals
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)
@@ -190,7 +204,7 @@ func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON,
},
Data: map[string][]byte{
secretDataKey: buf,
secretVersionKey: s.secretVersion,
secretVersionKey: []byte(secretVersion),
},
Type: s.secretType,
}, nil

View File

@@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package crud
@@ -793,7 +793,8 @@ func TestStorage(t *testing.T) {
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.True(t, errors.Is(err, ErrSecretTypeMismatch))
require.EqualError(t, err, "secret storage data has incorrect type: storage.pinniped.dev/candies-not must equal storage.pinniped.dev/candies")
require.EqualError(t, err, "error during get for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: "+
"secret storage data has incorrect type: storage.pinniped.dev/candies-not must equal storage.pinniped.dev/candies")
return nil
},
@@ -856,7 +857,8 @@ func TestStorage(t *testing.T) {
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.True(t, errors.Is(err, ErrSecretLabelMismatch))
require.EqualError(t, err, "secret storage data has incorrect label: candies-are-bad must equal candies")
require.EqualError(t, err, "error during get for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: "+
"secret storage data has incorrect label: candies-are-bad must equal candies")
return nil
},
@@ -919,7 +921,8 @@ func TestStorage(t *testing.T) {
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.True(t, errors.Is(err, ErrSecretVersionMismatch))
require.EqualError(t, err, "secret storage data has incorrect version")
require.EqualError(t, err, "error during get for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: "+
"secret storage data has incorrect version")
return nil
},
@@ -981,7 +984,8 @@ func TestStorage(t *testing.T) {
rv1, err := storage.Get(ctx, signature, out)
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.EqualError(t, err, "failed to decode candies for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: invalid character '}' looking for beginning of value")
require.EqualError(t, err, "error during get for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: "+
"failed to decode candies: invalid character '}' looking for beginning of value")
return nil
},
@@ -1095,3 +1099,155 @@ func errString(err error) string {
return err.Error()
}
func TestFromSecret(t *testing.T) {
fakeNow := time.Date(2030, time.January, 1, 0, 0, 0, 0, time.UTC)
lifetime := time.Minute * 10
fakeNowPlusLifetimeAsString := metav1.Time{Time: fakeNow.Add(lifetime)}.Format(time.RFC3339)
type testJSON struct {
Data string
}
tests := []struct {
name string
resource string
secret *corev1.Secret
wantData *testJSON
wantErr string
}{
{
name: "happy path",
resource: "candies",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: "some-namespace",
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev/type": "candies",
},
Annotations: map[string]string{
"storage.pinniped.dev/garbage-collect-after": fakeNowPlusLifetimeAsString,
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"snorlax"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies",
},
wantData: &testJSON{Data: "snorlax"},
},
{
name: "can't unmarshal",
resource: "candies",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: "some-namespace",
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev/type": "candies",
},
Annotations: map[string]string{
"storage.pinniped.dev/garbage-collect-after": fakeNowPlusLifetimeAsString,
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`not-json`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies",
},
wantData: &testJSON{Data: "snorlax"},
wantErr: "failed to decode candies: invalid character 'o' in literal null (expecting 'u')",
},
{
name: "wrong storage version",
resource: "candies",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: "some-namespace",
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev/type": "candies",
},
Annotations: map[string]string{
"storage.pinniped.dev/garbage-collect-after": fakeNowPlusLifetimeAsString,
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"snorlax"}`),
"pinniped-storage-version": []byte("wrong-version-here"),
},
Type: "storage.pinniped.dev/candies",
},
wantData: &testJSON{Data: "snorlax"},
wantErr: "secret storage data has incorrect version",
},
{
name: "wrong type label",
resource: "candies",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: "some-namespace",
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev/type": "candies",
},
Annotations: map[string]string{
"storage.pinniped.dev/garbage-collect-after": fakeNowPlusLifetimeAsString,
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"snorlax"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/not-candies",
},
wantData: &testJSON{Data: "snorlax"},
wantErr: "secret storage data has incorrect type: storage.pinniped.dev/not-candies must equal storage.pinniped.dev/candies",
},
{
name: "wrong secret type",
resource: "candies",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: "some-namespace",
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev/type": "candies",
},
Annotations: map[string]string{
"storage.pinniped.dev/garbage-collect-after": fakeNowPlusLifetimeAsString,
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"snorlax"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/not-candies",
},
wantData: &testJSON{Data: "snorlax"},
wantErr: "secret storage data has incorrect type: storage.pinniped.dev/not-candies must equal storage.pinniped.dev/candies",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
data := &testJSON{}
err := FromSecret("candies", tt.secret, data)
if tt.wantErr == "" {
require.NoError(t, err)
require.Equal(t, data, tt.wantData)
} else {
require.EqualError(t, err, tt.wantErr)
}
})
}
}