Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle Entra auth for ASO API managed clusters #5211

Merged
merged 2 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions azure/credential_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"sync"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)

type credentialCache struct {
mut *sync.Mutex
cache map[credentialCacheKey]azcore.TokenCredential
credFactory credentialFactory
}

type credentialFactory interface {
newClientSecretCredential(tenantID string, clientID string, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error)
newClientCertificateCredential(tenantID string, clientID string, clientCertificate []byte, clientCertificatePassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error)
newManagedIdentityCredential(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error)
newWorkloadIdentityCredential(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error)
}

// CredentialType represents the auth mechanism in use.
type CredentialType int

const (
// CredentialTypeClientSecret is for Service Principals with Client Secrets.
CredentialTypeClientSecret CredentialType = iota
// CredentialTypeClientCert is for Service Principals with Client certificates.
CredentialTypeClientCert
// CredentialTypeManagedIdentity is for Managed Identities.
CredentialTypeManagedIdentity
// CredentialTypeWorkloadIdentity is for Workload Identity.
CredentialTypeWorkloadIdentity
)

type credentialCacheKey struct {
authorityHost string
credentialType CredentialType
tenantID string
clientID string
secret string
}

// NewCredentialCache creates a new, empty CredentialCache.
func NewCredentialCache() CredentialCache {
return &credentialCache{
mut: new(sync.Mutex),
cache: make(map[credentialCacheKey]azcore.TokenCredential),
credFactory: azureCredentialFactory{},
}

Check warning on line 67 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L62-L67

Added lines #L62 - L67 were not covered by tests
}

func (c *credentialCache) GetOrStoreClientSecret(tenantID, clientID, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) {
return c.getOrStore(
credentialCacheKey{
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
credentialType: CredentialTypeClientSecret,
tenantID: tenantID,
clientID: clientID,
secret: clientSecret,
},
func() (azcore.TokenCredential, error) {
return c.credFactory.newClientSecretCredential(tenantID, clientID, clientSecret, opts)
},

Check warning on line 81 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L70-L81

Added lines #L70 - L81 were not covered by tests
)
}

func (c *credentialCache) GetOrStoreClientCert(tenantID, clientID string, cert, certPassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) {
return c.getOrStore(
credentialCacheKey{
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
credentialType: CredentialTypeClientCert,
tenantID: tenantID,
clientID: clientID,
nojnhuh marked this conversation as resolved.
Show resolved Hide resolved
secret: string(append(cert, certPassword...)),
},
func() (azcore.TokenCredential, error) {
return c.credFactory.newClientCertificateCredential(tenantID, clientID, cert, certPassword, opts)
},

Check warning on line 96 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L85-L96

Added lines #L85 - L96 were not covered by tests
)
}

func (c *credentialCache) GetOrStoreManagedIdentity(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) {
return c.getOrStore(
credentialCacheKey{
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
credentialType: CredentialTypeManagedIdentity,
// tenantID not used for managed identity
clientID: opts.ID.String(),
},
func() (azcore.TokenCredential, error) {
return c.credFactory.newManagedIdentityCredential(opts)
},

Check warning on line 110 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L100-L110

Added lines #L100 - L110 were not covered by tests
)
}

func (c *credentialCache) GetOrStoreWorkloadIdentity(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) {
return c.getOrStore(
credentialCacheKey{
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
credentialType: CredentialTypeWorkloadIdentity,
tenantID: opts.TenantID,
clientID: opts.ClientID,
},
func() (azcore.TokenCredential, error) {
return c.credFactory.newWorkloadIdentityCredential(opts)
},

Check warning on line 124 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L114-L124

Added lines #L114 - L124 were not covered by tests
)
}

func (c *credentialCache) getOrStore(key credentialCacheKey, newCredFunc func() (azcore.TokenCredential, error)) (azcore.TokenCredential, error) {
c.mut.Lock()
defer c.mut.Unlock()
if cred, exists := c.cache[key]; exists {
return cred, nil
}
cred, err := newCredFunc()
if err != nil {
return nil, err
}
c.cache[key] = cred
return cred, nil
}

type azureCredentialFactory struct{}

func (azureCredentialFactory) newClientSecretCredential(tenantID string, clientID string, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) {
return azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, opts)

Check warning on line 145 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L144-L145

Added lines #L144 - L145 were not covered by tests
}

func (azureCredentialFactory) newClientCertificateCredential(tenantID string, clientID string, clientCertificate []byte, clientCertificatePassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) {
certs, certKey, err := azidentity.ParseCertificates(clientCertificate, clientCertificatePassword)
if err != nil {
return nil, err
}
return azidentity.NewClientCertificateCredential(tenantID, clientID, certs, certKey, opts)

Check warning on line 153 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L148-L153

Added lines #L148 - L153 were not covered by tests
}

func (azureCredentialFactory) newManagedIdentityCredential(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) {
return azidentity.NewManagedIdentityCredential(opts)

Check warning on line 157 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L156-L157

Added lines #L156 - L157 were not covered by tests
}

func (azureCredentialFactory) newWorkloadIdentityCredential(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) {
return azidentity.NewWorkloadIdentityCredential(opts)

Check warning on line 161 in azure/credential_cache.go

View check run for this annotation

Codecov / codecov/patch

azure/credential_cache.go#L160-L161

Added lines #L160 - L161 were not covered by tests
}
101 changes: 101 additions & 0 deletions azure/credential_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"context"
"strconv"
"sync"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
. "github.com/onsi/gomega"
"github.com/pkg/errors"
)

type fakeTokenCredential struct {
tenantID string
}

func (t fakeTokenCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
return azcore.AccessToken{}, nil
}

func TestGetOrStore(t *testing.T) {
g := NewGomegaWithT(t)

credCache := &credentialCache{
mut: new(sync.Mutex),
cache: make(map[credentialCacheKey]azcore.TokenCredential),
}

newCredCount := 0
newCredFunc := func(cred fakeTokenCredential, err error) func() (azcore.TokenCredential, error) {
return func() (azcore.TokenCredential, error) {
newCredCount++
return cred, err
}
}

// the first call for a new key should invoke newCredFunc
cred, err := credCache.getOrStore(credentialCacheKey{tenantID: "1"}, newCredFunc(fakeTokenCredential{tenantID: "1"}, nil))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(cred).To(Equal(fakeTokenCredential{tenantID: "1"}))
g.Expect(newCredCount).To(Equal(1))

// subsequent calls for the same key should not create a new credential
cred, err = credCache.getOrStore(credentialCacheKey{tenantID: "1"}, newCredFunc(fakeTokenCredential{tenantID: "1"}, nil))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(cred).To(Equal(fakeTokenCredential{tenantID: "1"}))
g.Expect(newCredCount).To(Equal(1))
cred, err = credCache.getOrStore(credentialCacheKey{tenantID: "1"}, newCredFunc(fakeTokenCredential{tenantID: "1"}, nil))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(cred).To(Equal(fakeTokenCredential{tenantID: "1"}))
g.Expect(newCredCount).To(Equal(1))

expectedErr := errors.New("an error")
cred, err = credCache.getOrStore(credentialCacheKey{tenantID: "2"}, newCredFunc(fakeTokenCredential{tenantID: "2"}, expectedErr))
g.Expect(err).To(MatchError(expectedErr))
g.Expect(cred).To(BeNil())
g.Expect(newCredCount).To(Equal(2))
}

func TestGetOrStoreRace(t *testing.T) {
// This test makes no assertions, it only fails when the race detector finds race conditions.

credCache := &credentialCache{
mut: new(sync.Mutex),
cache: make(map[credentialCacheKey]azcore.TokenCredential),
}
newCredFunc := func(cred fakeTokenCredential, err error) func() (azcore.TokenCredential, error) {
return func() (azcore.TokenCredential, error) {
return cred, err
}
}

wg := new(sync.WaitGroup)
n := 1000
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _ = credCache.getOrStore(credentialCacheKey{tenantID: strconv.Itoa(i % 100)}, newCredFunc(fakeTokenCredential{}, nil))
}()
}
wg.Wait()
}
9 changes: 9 additions & 0 deletions azure/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
Expand Down Expand Up @@ -164,3 +165,11 @@ type ASOResourceSpecGetter[T genruntime.MetaObject] interface {
// non-ASO-backed CAPZ and should be considered eligible for adoption.
WasManaged(T) bool
}

// CredentialCache caches azcore.TokenCredentials.
type CredentialCache interface {
GetOrStoreClientSecret(tenantID, clientID, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error)
GetOrStoreClientCert(tenantID, clientID string, cert, certPassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error)
GetOrStoreManagedIdentity(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error)
GetOrStoreWorkloadIdentity(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error)
}
84 changes: 84 additions & 0 deletions azure/mock_azure/azure_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading