Skip to content

Commit 89422a7

Browse files
committed
handle Entra auth for ASO API managed clusters
1 parent a8ecbaa commit 89422a7

File tree

6 files changed

+1100
-8
lines changed

6 files changed

+1100
-8
lines changed

controllers/aso_credential_cache.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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 controllers
18+
19+
import (
20+
"context"
21+
"os"
22+
"strconv"
23+
24+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
25+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
26+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
27+
"github.com/Azure/azure-sdk-for-go/sdk/tracing/azotel"
28+
asoannotations "github.com/Azure/azure-service-operator/v2/pkg/common/annotations"
29+
"github.com/Azure/azure-service-operator/v2/pkg/common/config"
30+
corev1 "k8s.io/api/core/v1"
31+
"sigs.k8s.io/controller-runtime/pkg/client"
32+
33+
"sigs.k8s.io/cluster-api-provider-azure/azure"
34+
"sigs.k8s.io/cluster-api-provider-azure/pkg/ot"
35+
"sigs.k8s.io/cluster-api-provider-azure/util/tele"
36+
)
37+
38+
const (
39+
asoNamespaceSecretName = "aso-credential"
40+
asoGlobalSecretName = "aso-controller-settings" //nolint:gosec // This is not a secret, only a reference to one.
41+
asoNamespaceAnnotation = "serviceoperator.azure.com/operator-namespace"
42+
)
43+
44+
// ASOCredentialCache caches credentials defined for ASO resources.
45+
type ASOCredentialCache interface {
46+
authTokenForASOResource(context.Context, client.Object) (azcore.TokenCredential, error)
47+
}
48+
49+
type asoCredentialCache struct {
50+
cache azure.CredentialCache
51+
client client.Client
52+
}
53+
54+
// NewASOCredentialCache creates a new ASOCredentialCache.
55+
func NewASOCredentialCache(cache azure.CredentialCache, client client.Client) ASOCredentialCache {
56+
return &asoCredentialCache{
57+
cache: cache,
58+
client: client,
59+
}
60+
}
61+
62+
func (c *asoCredentialCache) authTokenForASOResource(ctx context.Context, obj client.Object) (azcore.TokenCredential, error) {
63+
ctx, _, done := tele.StartSpanWithLogger(ctx, "controllers.asoCredentialCache.authTokenForASOResource")
64+
defer done()
65+
66+
clientOpts, err := c.clientOptsForASOResource(ctx, obj)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
secretName := asoNamespaceSecretName
72+
if resourceSecretName := obj.GetAnnotations()[asoannotations.PerResourceSecret]; resourceSecretName != "" {
73+
secretName = resourceSecretName
74+
}
75+
secret := &corev1.Secret{}
76+
err = c.client.Get(ctx, client.ObjectKey{Namespace: obj.GetNamespace(), Name: secretName}, secret)
77+
if client.IgnoreNotFound(err) != nil {
78+
return nil, err
79+
}
80+
if err == nil {
81+
return c.authTokenForScopedASOSecret(secret, clientOpts)
82+
}
83+
84+
secretNamespace := obj.GetAnnotations()[asoNamespaceAnnotation]
85+
err = c.client.Get(ctx, client.ObjectKey{Namespace: secretNamespace, Name: asoGlobalSecretName}, secret)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
return c.authTokenForGlobalASOSecret(secret, clientOpts)
91+
}
92+
93+
func (c *asoCredentialCache) clientOptsForASOResource(ctx context.Context, obj client.Object) (azcore.ClientOptions, error) {
94+
secretNamespace := obj.GetAnnotations()[asoNamespaceAnnotation]
95+
secret := &corev1.Secret{}
96+
err := c.client.Get(ctx, client.ObjectKey{Namespace: secretNamespace, Name: asoGlobalSecretName}, secret)
97+
if client.IgnoreNotFound(err) != nil {
98+
return azcore.ClientOptions{}, err
99+
}
100+
101+
otelTP, err := ot.OTLPTracerProvider(ctx)
102+
if err != nil {
103+
return azcore.ClientOptions{}, err
104+
}
105+
106+
opts := azcore.ClientOptions{
107+
TracingProvider: azotel.NewTracingProvider(otelTP, nil),
108+
Cloud: cloud.Configuration{
109+
ActiveDirectoryAuthorityHost: string(secret.Data[config.AzureAuthorityHost]),
110+
},
111+
}
112+
113+
if len(secret.Data[config.ResourceManagerAudience]) > 0 ||
114+
len(secret.Data[config.ResourceManagerEndpoint]) > 0 {
115+
opts.Cloud.Services = map[cloud.ServiceName]cloud.ServiceConfiguration{
116+
cloud.ResourceManager: {
117+
Audience: string(secret.Data[config.ResourceManagerAudience]),
118+
Endpoint: string(secret.Data[config.ResourceManagerEndpoint]),
119+
},
120+
}
121+
}
122+
123+
return opts, nil
124+
}
125+
126+
func (c *asoCredentialCache) authTokenForScopedASOSecret(secret *corev1.Secret, clientOpts azcore.ClientOptions) (azcore.TokenCredential, error) {
127+
d := secret.Data
128+
129+
if _, hasSecret := d[config.AzureClientSecret]; hasSecret {
130+
return c.cache.GetOrStoreClientSecret(
131+
string(d[config.AzureTenantID]),
132+
string(d[config.AzureClientID]),
133+
string(d[config.AzureClientSecret]),
134+
&azidentity.ClientSecretCredentialOptions{
135+
ClientOptions: clientOpts,
136+
},
137+
)
138+
}
139+
140+
if _, hasCert := d[config.AzureClientCertificate]; hasCert {
141+
return c.cache.GetOrStoreClientCert(
142+
string(d[config.AzureTenantID]),
143+
string(d[config.AzureClientID]),
144+
d[config.AzureClientCertificate],
145+
d[config.AzureClientCertificatePassword],
146+
&azidentity.ClientCertificateCredentialOptions{
147+
ClientOptions: clientOpts,
148+
},
149+
)
150+
}
151+
152+
if authMode := d[config.AuthMode]; config.AuthModeOption(authMode) == config.PodIdentityAuthMode {
153+
return c.cache.GetOrStoreManagedIdentity(
154+
&azidentity.ManagedIdentityCredentialOptions{
155+
ClientOptions: clientOpts,
156+
ID: azidentity.ClientID(d[config.AzureClientID]),
157+
},
158+
)
159+
}
160+
161+
return c.cache.GetOrStoreWorkloadIdentity(
162+
&azidentity.WorkloadIdentityCredentialOptions{
163+
ClientOptions: clientOpts,
164+
TenantID: string(d[config.AzureTenantID]),
165+
ClientID: string(d[config.AzureClientID]),
166+
TokenFilePath: federatedTokenFilePath(),
167+
},
168+
)
169+
}
170+
171+
func (c *asoCredentialCache) authTokenForGlobalASOSecret(secret *corev1.Secret, clientOpts azcore.ClientOptions) (azcore.TokenCredential, error) {
172+
d := secret.Data
173+
174+
if workloadID, _ := strconv.ParseBool(string(d[config.UseWorkloadIdentityAuth])); workloadID {
175+
return c.cache.GetOrStoreWorkloadIdentity(
176+
&azidentity.WorkloadIdentityCredentialOptions{
177+
ClientOptions: clientOpts,
178+
TenantID: string(d[config.AzureTenantID]),
179+
ClientID: string(d[config.AzureClientID]),
180+
TokenFilePath: federatedTokenFilePath(),
181+
},
182+
)
183+
}
184+
185+
if _, hasSecret := d[config.AzureClientSecret]; hasSecret {
186+
return c.cache.GetOrStoreClientSecret(
187+
string(d[config.AzureTenantID]),
188+
string(d[config.AzureClientID]),
189+
string(d[config.AzureClientSecret]),
190+
&azidentity.ClientSecretCredentialOptions{
191+
ClientOptions: clientOpts,
192+
},
193+
)
194+
}
195+
196+
if _, hasCert := d[config.AzureClientCertificate]; hasCert {
197+
return c.cache.GetOrStoreClientCert(
198+
string(d[config.AzureTenantID]),
199+
string(d[config.AzureClientID]),
200+
d[config.AzureClientCertificate],
201+
d[config.AzureClientCertificatePassword],
202+
&azidentity.ClientCertificateCredentialOptions{
203+
ClientOptions: clientOpts,
204+
},
205+
)
206+
}
207+
208+
return c.cache.GetOrStoreManagedIdentity(
209+
&azidentity.ManagedIdentityCredentialOptions{
210+
ClientOptions: clientOpts,
211+
ID: azidentity.ClientID(d[config.AzureClientID]),
212+
},
213+
)
214+
}
215+
216+
func federatedTokenFilePath() string {
217+
if env, ok := os.LookupEnv("AZURE_FEDERATED_TOKEN_FILE"); ok {
218+
return env
219+
}
220+
return "/var/run/secrets/azure/tokens/azure-identity-token"
221+
}

0 commit comments

Comments
 (0)