master
go 182 lines 4.74 KB
Raw
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 }