Skip to content

Commit b7c4459

Browse files
committed
[RFC-0010] Add core auth library
Signed-off-by: Matheus Pimenta <[email protected]>
1 parent f7fc565 commit b7c4459

File tree

16 files changed

+989
-105
lines changed

16 files changed

+989
-105
lines changed

auth/doc.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 The Flux authors
2+
Copyright 2025 The Flux authors
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -14,6 +14,5 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package auth is a Go package for OIDC-based authentication against Git SaaS providers.
18-
// Includes support for Azure DevOps and GitHub Apps.
17+
// auth is a package for handling secret-less authentication with cloud providers.
1918
package auth

auth/get_token.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
Copyright 2025 The Flux 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 auth
18+
19+
import (
20+
"context"
21+
"crypto/sha256"
22+
"fmt"
23+
"os"
24+
"strings"
25+
26+
authnv1 "k8s.io/api/authentication/v1"
27+
corev1 "k8s.io/api/core/v1"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
30+
"github.com/fluxcd/pkg/cache"
31+
)
32+
33+
// GetToken returns an access token for accessing resources in the given cloud provider.
34+
func GetToken(ctx context.Context, provider Provider, opts ...Option) (Token, error) {
35+
36+
var o Options
37+
o.Apply(opts...)
38+
39+
// Initialize default token fetcher.
40+
newAccessToken := func() (Token, error) {
41+
token, err := provider.NewDefaultToken(ctx, opts...)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to create default access token: %w", err)
44+
}
45+
return token, nil
46+
}
47+
48+
// Initialize service account token fetcher if service account is specified.
49+
var providerAudience, providerIdentity string
50+
var serviceAccountP *corev1.ServiceAccount
51+
if o.ServiceAccount != nil {
52+
// Get service account and prepare a function to create a token for it.
53+
var serviceAccount corev1.ServiceAccount
54+
if err := o.Client.Get(ctx, *o.ServiceAccount, &serviceAccount); err != nil {
55+
return nil, fmt.Errorf("failed to get service account: %w", err)
56+
}
57+
serviceAccountP = &serviceAccount
58+
59+
// Get provider audience.
60+
var err error
61+
providerAudience, err = provider.GetAudience(ctx, serviceAccount)
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to get provider audience: %w", err)
64+
}
65+
66+
// Get provider identity.
67+
providerIdentity, err = provider.GetIdentity(serviceAccount)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to get provider identity: %w", err)
70+
}
71+
72+
// Initialize access token fetcher that will use the identity token.
73+
newAccessToken = func() (Token, error) {
74+
identityToken, err := newServiceAccountToken(ctx, o.Client, serviceAccount, providerAudience)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
token, err := provider.NewTokenForServiceAccount(ctx, identityToken, serviceAccount, opts...)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to create access token: %w", err)
82+
}
83+
84+
return token, nil
85+
}
86+
}
87+
88+
// Initialize registry token fetcher if container registry is specified.
89+
newToken := newAccessToken
90+
if o.ArtifactRepository != "" {
91+
newToken = func() (Token, error) {
92+
accessToken, err := newAccessToken()
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
token, err := provider.NewRegistryToken(ctx, o.ArtifactRepository, accessToken, opts...)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to create container registry login: %w", err)
100+
}
101+
102+
return token, nil
103+
}
104+
}
105+
106+
// Bail out early if cache is disabled.
107+
if o.Cache == nil {
108+
return newToken()
109+
}
110+
111+
// Build cache key.
112+
cacheKey := buildCacheKey(provider, providerAudience, providerIdentity, serviceAccountP, opts...)
113+
114+
// Get involved object details.
115+
kind := o.InvolvedObject.Kind
116+
name := o.InvolvedObject.Name
117+
namespace := o.InvolvedObject.Namespace
118+
119+
// Get token from cache.
120+
token, _, err := o.Cache.GetOrSet(ctx, cacheKey, func(ctx context.Context) (cache.Token, error) {
121+
return newToken()
122+
}, cache.WithInvolvedObject(kind, name, namespace))
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
return token, nil
128+
}
129+
130+
func newServiceAccountToken(ctx context.Context, client client.Client,
131+
serviceAccount corev1.ServiceAccount, providerAudience string) (string, error) {
132+
133+
// Some providers require a pod to be bound to the token request.
134+
// It doesn't really matter which pod, the identity in the token
135+
// is still the service account, so we just use the current pod
136+
// name.
137+
var boundObjectRef *authnv1.BoundObjectReference
138+
if podName := os.Getenv("HOSTNAME"); podName != "" {
139+
boundObjectRef = &authnv1.BoundObjectReference{
140+
Kind: "Pod",
141+
Name: podName,
142+
}
143+
}
144+
145+
tokenReq := &authnv1.TokenRequest{
146+
Spec: authnv1.TokenRequestSpec{
147+
Audiences: []string{providerAudience},
148+
BoundObjectRef: boundObjectRef,
149+
},
150+
}
151+
if err := client.SubResource("token").Create(ctx, &serviceAccount, tokenReq); err != nil {
152+
return "", fmt.Errorf("failed to create kubernetes service account token: %w", err)
153+
}
154+
return tokenReq.Status.Token, nil
155+
}
156+
157+
func buildCacheKey(provider Provider, providerAudience, providerIdentity string,
158+
serviceAccount *corev1.ServiceAccount, opts ...Option) string {
159+
160+
var o Options
161+
o.Apply(opts...)
162+
163+
var keyParts []string
164+
165+
keyParts = append(keyParts, fmt.Sprintf("provider=%s", provider.GetName()))
166+
167+
if serviceAccount != nil {
168+
keyParts = append(keyParts, fmt.Sprintf("providerAudience=%s", providerAudience))
169+
keyParts = append(keyParts, fmt.Sprintf("providerIdentity=%s", providerIdentity))
170+
keyParts = append(keyParts, fmt.Sprintf("serviceAccountName=%s", serviceAccount.Name))
171+
keyParts = append(keyParts, fmt.Sprintf("serviceAccountNamespace=%s", serviceAccount.Namespace))
172+
}
173+
174+
if len(o.Scopes) > 0 {
175+
keyParts = append(keyParts, fmt.Sprintf("scopes=%s", strings.Join(o.Scopes, ",")))
176+
}
177+
178+
if o.ArtifactRepository != "" {
179+
keyParts = append(keyParts, fmt.Sprintf("artifactRepositoryKey=%s", provider.GetArtifactCacheKey(o.ArtifactRepository)))
180+
}
181+
182+
if o.STSEndpoint != "" {
183+
keyParts = append(keyParts, fmt.Sprintf("stsEndpoint=%s", o.STSEndpoint))
184+
}
185+
186+
if o.ProxyURL != nil {
187+
keyParts = append(keyParts, fmt.Sprintf("proxyURL=%s", o.ProxyURL.String()))
188+
}
189+
190+
s := strings.Join(keyParts, ",")
191+
hash := sha256.Sum256([]byte(s))
192+
return fmt.Sprintf("%x", hash)
193+
}

0 commit comments

Comments
 (0)