Skip to content

Commit 7e9ebb9

Browse files
authored
Merge pull request #5211 from nojnhuh/asoapi-aad
handle Entra auth for ASO API managed clusters
2 parents eb758cc + dd9b8dc commit 7e9ebb9

10 files changed

+1456
-8
lines changed

azure/credential_cache.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package azure
18+
19+
import (
20+
"sync"
21+
22+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
23+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
24+
)
25+
26+
type credentialCache struct {
27+
mut *sync.Mutex
28+
cache map[credentialCacheKey]azcore.TokenCredential
29+
credFactory credentialFactory
30+
}
31+
32+
type credentialFactory interface {
33+
newClientSecretCredential(tenantID string, clientID string, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error)
34+
newClientCertificateCredential(tenantID string, clientID string, clientCertificate []byte, clientCertificatePassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error)
35+
newManagedIdentityCredential(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error)
36+
newWorkloadIdentityCredential(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error)
37+
}
38+
39+
// CredentialType represents the auth mechanism in use.
40+
type CredentialType int
41+
42+
const (
43+
// CredentialTypeClientSecret is for Service Principals with Client Secrets.
44+
CredentialTypeClientSecret CredentialType = iota
45+
// CredentialTypeClientCert is for Service Principals with Client certificates.
46+
CredentialTypeClientCert
47+
// CredentialTypeManagedIdentity is for Managed Identities.
48+
CredentialTypeManagedIdentity
49+
// CredentialTypeWorkloadIdentity is for Workload Identity.
50+
CredentialTypeWorkloadIdentity
51+
)
52+
53+
type credentialCacheKey struct {
54+
authorityHost string
55+
credentialType CredentialType
56+
tenantID string
57+
clientID string
58+
secret string
59+
}
60+
61+
// NewCredentialCache creates a new, empty CredentialCache.
62+
func NewCredentialCache() CredentialCache {
63+
return &credentialCache{
64+
mut: new(sync.Mutex),
65+
cache: make(map[credentialCacheKey]azcore.TokenCredential),
66+
credFactory: azureCredentialFactory{},
67+
}
68+
}
69+
70+
func (c *credentialCache) GetOrStoreClientSecret(tenantID, clientID, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) {
71+
return c.getOrStore(
72+
credentialCacheKey{
73+
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
74+
credentialType: CredentialTypeClientSecret,
75+
tenantID: tenantID,
76+
clientID: clientID,
77+
secret: clientSecret,
78+
},
79+
func() (azcore.TokenCredential, error) {
80+
return c.credFactory.newClientSecretCredential(tenantID, clientID, clientSecret, opts)
81+
},
82+
)
83+
}
84+
85+
func (c *credentialCache) GetOrStoreClientCert(tenantID, clientID string, cert, certPassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) {
86+
return c.getOrStore(
87+
credentialCacheKey{
88+
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
89+
credentialType: CredentialTypeClientCert,
90+
tenantID: tenantID,
91+
clientID: clientID,
92+
secret: string(append(cert, certPassword...)),
93+
},
94+
func() (azcore.TokenCredential, error) {
95+
return c.credFactory.newClientCertificateCredential(tenantID, clientID, cert, certPassword, opts)
96+
},
97+
)
98+
}
99+
100+
func (c *credentialCache) GetOrStoreManagedIdentity(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) {
101+
return c.getOrStore(
102+
credentialCacheKey{
103+
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
104+
credentialType: CredentialTypeManagedIdentity,
105+
// tenantID not used for managed identity
106+
clientID: opts.ID.String(),
107+
},
108+
func() (azcore.TokenCredential, error) {
109+
return c.credFactory.newManagedIdentityCredential(opts)
110+
},
111+
)
112+
}
113+
114+
func (c *credentialCache) GetOrStoreWorkloadIdentity(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) {
115+
return c.getOrStore(
116+
credentialCacheKey{
117+
authorityHost: opts.Cloud.ActiveDirectoryAuthorityHost,
118+
credentialType: CredentialTypeWorkloadIdentity,
119+
tenantID: opts.TenantID,
120+
clientID: opts.ClientID,
121+
},
122+
func() (azcore.TokenCredential, error) {
123+
return c.credFactory.newWorkloadIdentityCredential(opts)
124+
},
125+
)
126+
}
127+
128+
func (c *credentialCache) getOrStore(key credentialCacheKey, newCredFunc func() (azcore.TokenCredential, error)) (azcore.TokenCredential, error) {
129+
c.mut.Lock()
130+
defer c.mut.Unlock()
131+
if cred, exists := c.cache[key]; exists {
132+
return cred, nil
133+
}
134+
cred, err := newCredFunc()
135+
if err != nil {
136+
return nil, err
137+
}
138+
c.cache[key] = cred
139+
return cred, nil
140+
}
141+
142+
type azureCredentialFactory struct{}
143+
144+
func (azureCredentialFactory) newClientSecretCredential(tenantID string, clientID string, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) {
145+
return azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, opts)
146+
}
147+
148+
func (azureCredentialFactory) newClientCertificateCredential(tenantID string, clientID string, clientCertificate []byte, clientCertificatePassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) {
149+
certs, certKey, err := azidentity.ParseCertificates(clientCertificate, clientCertificatePassword)
150+
if err != nil {
151+
return nil, err
152+
}
153+
return azidentity.NewClientCertificateCredential(tenantID, clientID, certs, certKey, opts)
154+
}
155+
156+
func (azureCredentialFactory) newManagedIdentityCredential(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) {
157+
return azidentity.NewManagedIdentityCredential(opts)
158+
}
159+
160+
func (azureCredentialFactory) newWorkloadIdentityCredential(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) {
161+
return azidentity.NewWorkloadIdentityCredential(opts)
162+
}

azure/credential_cache_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package azure
18+
19+
import (
20+
"context"
21+
"strconv"
22+
"sync"
23+
"testing"
24+
25+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
26+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
27+
. "github.com/onsi/gomega"
28+
"github.com/pkg/errors"
29+
)
30+
31+
type fakeTokenCredential struct {
32+
tenantID string
33+
}
34+
35+
func (t fakeTokenCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
36+
return azcore.AccessToken{}, nil
37+
}
38+
39+
func TestGetOrStore(t *testing.T) {
40+
g := NewGomegaWithT(t)
41+
42+
credCache := &credentialCache{
43+
mut: new(sync.Mutex),
44+
cache: make(map[credentialCacheKey]azcore.TokenCredential),
45+
}
46+
47+
newCredCount := 0
48+
newCredFunc := func(cred fakeTokenCredential, err error) func() (azcore.TokenCredential, error) {
49+
return func() (azcore.TokenCredential, error) {
50+
newCredCount++
51+
return cred, err
52+
}
53+
}
54+
55+
// the first call for a new key should invoke newCredFunc
56+
cred, err := credCache.getOrStore(credentialCacheKey{tenantID: "1"}, newCredFunc(fakeTokenCredential{tenantID: "1"}, nil))
57+
g.Expect(err).NotTo(HaveOccurred())
58+
g.Expect(cred).To(Equal(fakeTokenCredential{tenantID: "1"}))
59+
g.Expect(newCredCount).To(Equal(1))
60+
61+
// subsequent calls for the same key should not create a new credential
62+
cred, err = credCache.getOrStore(credentialCacheKey{tenantID: "1"}, newCredFunc(fakeTokenCredential{tenantID: "1"}, nil))
63+
g.Expect(err).NotTo(HaveOccurred())
64+
g.Expect(cred).To(Equal(fakeTokenCredential{tenantID: "1"}))
65+
g.Expect(newCredCount).To(Equal(1))
66+
cred, err = credCache.getOrStore(credentialCacheKey{tenantID: "1"}, newCredFunc(fakeTokenCredential{tenantID: "1"}, nil))
67+
g.Expect(err).NotTo(HaveOccurred())
68+
g.Expect(cred).To(Equal(fakeTokenCredential{tenantID: "1"}))
69+
g.Expect(newCredCount).To(Equal(1))
70+
71+
expectedErr := errors.New("an error")
72+
cred, err = credCache.getOrStore(credentialCacheKey{tenantID: "2"}, newCredFunc(fakeTokenCredential{tenantID: "2"}, expectedErr))
73+
g.Expect(err).To(MatchError(expectedErr))
74+
g.Expect(cred).To(BeNil())
75+
g.Expect(newCredCount).To(Equal(2))
76+
}
77+
78+
func TestGetOrStoreRace(t *testing.T) {
79+
// This test makes no assertions, it only fails when the race detector finds race conditions.
80+
81+
credCache := &credentialCache{
82+
mut: new(sync.Mutex),
83+
cache: make(map[credentialCacheKey]azcore.TokenCredential),
84+
}
85+
newCredFunc := func(cred fakeTokenCredential, err error) func() (azcore.TokenCredential, error) {
86+
return func() (azcore.TokenCredential, error) {
87+
return cred, err
88+
}
89+
}
90+
91+
wg := new(sync.WaitGroup)
92+
n := 1000
93+
for i := 0; i < n; i++ {
94+
wg.Add(1)
95+
go func() {
96+
defer wg.Done()
97+
_, _ = credCache.getOrStore(credentialCacheKey{tenantID: strconv.Itoa(i % 100)}, newCredFunc(fakeTokenCredential{}, nil))
98+
}()
99+
}
100+
wg.Wait()
101+
}

azure/interfaces.go

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
24+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
2425
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -164,3 +165,11 @@ type ASOResourceSpecGetter[T genruntime.MetaObject] interface {
164165
// non-ASO-backed CAPZ and should be considered eligible for adoption.
165166
WasManaged(T) bool
166167
}
168+
169+
// CredentialCache caches azcore.TokenCredentials.
170+
type CredentialCache interface {
171+
GetOrStoreClientSecret(tenantID, clientID, clientSecret string, opts *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error)
172+
GetOrStoreClientCert(tenantID, clientID string, cert, certPassword []byte, opts *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error)
173+
GetOrStoreManagedIdentity(opts *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error)
174+
GetOrStoreWorkloadIdentity(opts *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error)
175+
}

azure/mock_azure/azure_mock.go

+84
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)