| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package azure |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" |
| 13 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" |
| 14 | "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore" |
| 15 | "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/internal/httpx" |
| 16 | "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth" |
| 17 | ) |
| 18 | |
| 19 | const azureKeyVaultScope = "https://vault.azure.net/.default" |
| 20 | |
| 21 | func (s *store) init(_ context.Context) error { |
| 22 | switch { |
| 23 | case s.Config.Timeout.Duration() < 0: |
| 24 | return fmt.Errorf("timeout cannot be negative") |
| 25 | case s.Config.Timeout.Duration() == 0: |
| 26 | s.Config.Timeout = defaultTimeout |
| 27 | } |
| 28 | |
| 29 | if err := s.Config.ValidateWithPath(""); err != nil { |
| 30 | return err |
| 31 | } |
| 32 | s.runtime = &runtime{ |
| 33 | apiClient: httpx.APIClient(s.Config.Timeout.Duration()), |
| 34 | imdsClient: httpx.NoProxyClient(s.Config.Timeout.Duration()), |
| 35 | } |
| 36 | |
| 37 | cred, err := s.Config.NewCredentialWithOptions(s.credentialOptions()) |
| 38 | if err != nil { |
| 39 | return fmt.Errorf("creating azure credential for kind %q: %w", secretstore.KindAzureKV, err) |
| 40 | } |
| 41 | cred = credentialWithTimeout{ |
| 42 | cred: cred, |
| 43 | timeout: s.authTimeout(), |
| 44 | } |
| 45 | |
| 46 | tokenProvider, err := cloudauth.NewTokenProvider( |
| 47 | cred, |
| 48 | []string{azureKeyVaultScope}, |
| 49 | cloudauth.DefaultTokenRefreshMargin, |
| 50 | ) |
| 51 | if err != nil { |
| 52 | return fmt.Errorf("creating azure token provider for kind %q: %w", secretstore.KindAzureKV, err) |
| 53 | } |
| 54 | |
| 55 | s.published = &publishedStore{ |
| 56 | runtime: s.runtime, |
| 57 | tokenProvider: tokenProvider, |
| 58 | } |
| 59 | return nil |
| 60 | } |
| 61 | |
| 62 | func (s *store) authTimeout() time.Duration { |
| 63 | switch s.Config.NormalizedMode() { |
| 64 | case cloudauth.AzureADAuthModeServicePrincipal: |
| 65 | if s.runtime.apiClient != nil { |
| 66 | return s.runtime.apiClient.Timeout |
| 67 | } |
| 68 | case cloudauth.AzureADAuthModeManagedIdentity: |
| 69 | if s.runtime.imdsClient != nil { |
| 70 | return s.runtime.imdsClient.Timeout |
| 71 | } |
| 72 | case cloudauth.AzureADAuthModeDefault: |
| 73 | if s.runtime.apiClient != nil && s.runtime.apiClient.Timeout > 0 { |
| 74 | return s.runtime.apiClient.Timeout |
| 75 | } |
| 76 | if s.runtime.imdsClient != nil { |
| 77 | return s.runtime.imdsClient.Timeout |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | return 0 |
| 82 | } |
| 83 | |
| 84 | func (s *store) credentialOptions() *cloudauth.AzureADCredentialOptions { |
| 85 | opts := &cloudauth.AzureADCredentialOptions{} |
| 86 | |
| 87 | switch s.Config.NormalizedMode() { |
| 88 | case cloudauth.AzureADAuthModeServicePrincipal: |
| 89 | if s.runtime.apiClient != nil && s.runtime.apiClient.Transport != nil { |
| 90 | opts.ClientOptions.Transport = transportAdapter{s.runtime.apiClient.Transport} |
| 91 | } |
| 92 | case cloudauth.AzureADAuthModeManagedIdentity: |
| 93 | if s.runtime.imdsClient != nil && s.runtime.imdsClient.Transport != nil { |
| 94 | opts.ClientOptions.Transport = transportAdapter{s.runtime.imdsClient.Transport} |
| 95 | } |
| 96 | case cloudauth.AzureADAuthModeDefault: |
| 97 | opts.ClientOptions.Transport = routingTransportAdapter{ |
| 98 | defaultRoundTripper: roundTripperForClient(s.runtime.apiClient), |
| 99 | noProxyRoundTripper: roundTripperForClient(s.runtime.imdsClient), |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | return opts |
| 104 | } |
| 105 | |
| 106 | type transportAdapter struct { |
| 107 | roundTripper httpRoundTripper |
| 108 | } |
| 109 | |
| 110 | type httpRoundTripper interface { |
| 111 | RoundTrip(*http.Request) (*http.Response, error) |
| 112 | } |
| 113 | |
| 114 | func (t transportAdapter) Do(req *http.Request) (*http.Response, error) { |
| 115 | return t.roundTripper.RoundTrip(req) |
| 116 | } |
| 117 | |
| 118 | type routingTransportAdapter struct { |
| 119 | defaultRoundTripper httpRoundTripper |
| 120 | noProxyRoundTripper httpRoundTripper |
| 121 | } |
| 122 | |
| 123 | func (t routingTransportAdapter) Do(req *http.Request) (*http.Response, error) { |
| 124 | switch { |
| 125 | case shouldUseNoProxyTransport(req) && t.noProxyRoundTripper != nil: |
| 126 | return t.noProxyRoundTripper.RoundTrip(req) |
| 127 | case t.defaultRoundTripper != nil: |
| 128 | return t.defaultRoundTripper.RoundTrip(req) |
| 129 | case t.noProxyRoundTripper != nil: |
| 130 | return t.noProxyRoundTripper.RoundTrip(req) |
| 131 | default: |
| 132 | return http.DefaultTransport.RoundTrip(req) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func roundTripperForClient(client *http.Client) httpRoundTripper { |
| 137 | if client != nil && client.Transport != nil { |
| 138 | return client.Transport |
| 139 | } |
| 140 | if rt, ok := http.DefaultTransport.(httpRoundTripper); ok { |
| 141 | return rt |
| 142 | } |
| 143 | return nil |
| 144 | } |
| 145 | |
| 146 | // Managed identity endpoints are local or link-local and must bypass proxies. |
| 147 | func shouldUseNoProxyTransport(req *http.Request) bool { |
| 148 | if req == nil || req.URL == nil { |
| 149 | return false |
| 150 | } |
| 151 | |
| 152 | host := req.URL.Hostname() |
| 153 | if host == "" { |
| 154 | return false |
| 155 | } |
| 156 | if host == "localhost" { |
| 157 | return true |
| 158 | } |
| 159 | |
| 160 | ip := net.ParseIP(host) |
| 161 | if ip == nil { |
| 162 | return false |
| 163 | } |
| 164 | |
| 165 | return ip.IsLoopback() || ip.IsLinkLocalUnicast() |
| 166 | } |
| 167 | |
| 168 | type credentialWithTimeout struct { |
| 169 | cred azcore.TokenCredential |
| 170 | timeout time.Duration |
| 171 | } |
| 172 | |
| 173 | func (c credentialWithTimeout) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { |
| 174 | if c.timeout <= 0 { |
| 175 | return c.cred.GetToken(ctx, opts) |
| 176 | } |
| 177 | |
| 178 | ctx, cancel := context.WithTimeout(ctx, c.timeout) |
| 179 | defer cancel() |
| 180 | |
| 181 | return c.cred.GetToken(ctx, opts) |
| 182 | } |