Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
4 changes: 2 additions & 2 deletions sdk/azidentity/default_azure_credential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ func TestDefaultAzureCredential_WorkloadIdentity(t *testing.T) {
t.Setenv(azureTokenCredentials, credNameWorkloadIdentity)
// these values should trigger validation errors if WorkloadIdentityCredential
// tries to configure identity binding mode...
t.Setenv(customtokenproxy.AzureKubernetesCAData, "not a valid cert")
t.Setenv(customtokenproxy.AzureKubernetesTokenProxy, "http://timeout.local&fail=yes#please")
t.Setenv(customtokenproxy.EnvAzureKubernetesCAData, "not a valid cert")
t.Setenv(customtokenproxy.EnvAzureKubernetesTokenProxy, "http://timeout.local&fail=yes#please")

cred, err := NewDefaultAzureCredential(&DefaultAzureCredentialOptions{
ClientOptions: policy.ClientOptions{Transport: &mockSTS{}},
Expand Down
8 changes: 8 additions & 0 deletions sdk/azidentity/go.work.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
121 changes: 121 additions & 0 deletions sdk/azidentity/internal/customtokenproxy/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package customtokenproxy

import (
"errors"
"fmt"
"net/url"
"os"

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

const (
EnvAzureKubernetesCAData = "AZURE_KUBERNETES_CA_DATA"
EnvAzureKubernetesCAFile = "AZURE_KUBERNETES_CA_FILE"
EnvAzureKubernetesSNIName = "AZURE_KUBERNETES_SNI_NAME"
EnvAzureKubernetesTokenProxy = "AZURE_KUBERNETES_TOKEN_PROXY"
)

func readOptionsFromEnv() *exported.CustomTokenProxyOptions {
return &exported.CustomTokenProxyOptions{
TokenProxy: os.Getenv(EnvAzureKubernetesTokenProxy),
SNIName: os.Getenv(EnvAzureKubernetesSNIName),
CAFile: os.Getenv(EnvAzureKubernetesCAFile),
CAData: os.Getenv(EnvAzureKubernetesCAData),
}
}

func backfillOptionsFromEnv(opts *exported.CustomTokenProxyOptions) {
if opts.CAData != "" || opts.CAFile != "" || opts.SNIName != "" || opts.TokenProxy != "" {
return
}

// only backfill if all fields are empty
*opts = *readOptionsFromEnv()
}

func parseTokenProxyURL(endpoint string) (*url.URL, error) {
tokenProxy, err := url.Parse(endpoint)
if err != nil {
return nil, fmt.Errorf("failed to parse custom token proxy URL %q: %s", endpoint, err)
}
if tokenProxy.Scheme != "https" {
return nil, fmt.Errorf("custom token endpoint must use https scheme, got %q", tokenProxy.Scheme)
}
if tokenProxy.User != nil {
return nil, fmt.Errorf("custom token endpoint URL %q must not contain user info", tokenProxy)
}
if tokenProxy.RawQuery != "" {
return nil, fmt.Errorf("custom token endpoint URL %q must not contain a query", tokenProxy)
}
if tokenProxy.EscapedFragment() != "" {
return nil, fmt.Errorf("custom token endpoint URL %q must not contain a fragment", tokenProxy)
}
if tokenProxy.EscapedPath() == "" {
// if the path is empty, set it to "/" to avoid stripping the path from req.URL
tokenProxy.Path = "/"
}
return tokenProxy, nil
}

var (
errCustomEndpointSetWithoutTokenProxy = errors.New(
"AZURE_KUBERNETES_TOKEN_PROXY is not set but other custom endpoint-related settings are present",
)
errCustomEndpointMultipleCASourcesSet = errors.New(
"only one of AzureKubernetesCAFile or AzureKubernetesCAData can be specified",
)
)

func noopConfigure(*policy.ClientOptions) {
// no-op
}

// GetClientOptionsConfigurer returns a function that configures the client options to use the custom token proxy.
func GetClientOptionsConfigurer(opts *exported.CustomTokenProxyOptions) (func(*policy.ClientOptions), error) {
if opts == nil {
return noopConfigure, nil
}

backfillOptionsFromEnv(opts)

if opts.TokenProxy == "" {
// custom token proxy is not set, while other Kubernetes-related environment variables are present,
// this is likely a configuration issue so erroring out to avoid misconfiguration
if opts.SNIName != "" || opts.CAFile != "" || opts.CAData != "" {
return nil, errCustomEndpointSetWithoutTokenProxy
}

return noopConfigure, nil
}

tokenProxy, err := parseTokenProxyURL(opts.TokenProxy)
if err != nil {
return nil, err
}

// CAFile and CAData are mutually exclusive, at most one can be set.
// If none of CAFile or CAData are set, the default system CA pool will be used.
if opts.CAFile != "" && opts.CAData != "" {
return nil, errCustomEndpointMultipleCASourcesSet
}

// preload the transport
t := &transport{
caFile: opts.CAFile,
caData: []byte(opts.CAData),
sniName: opts.SNIName,
tokenProxy: tokenProxy,
}
if _, err := t.getTokenTransporter(); err != nil {
return nil, err
}

return func(clientOptions *policy.ClientOptions) {
clientOptions.Transport = t
}, nil
}
Loading
Loading