master
go 93 lines 3.47 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package azure
4
5 import (
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "strings"
12
13 "github.com/netdata/netdata/go/plugins/logger"
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 )
17
18 func (s *publishedStore) Resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
19 return s.resolve(ctx, req)
20 }
21
22 func (s *publishedStore) resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
23 vaultName, secretName, ok := splitOperand(req.Operand)
24 if !ok {
25 return "", fmt.Errorf("resolving secret '%s': store '%s': operand must be in format 'vault-name/secret-name'", req.Original, req.StoreKey)
26 }
27 if !reAzureSafeName.MatchString(vaultName) {
28 return "", fmt.Errorf("resolving secret '%s': store '%s': invalid vault name '%s'", req.Original, req.StoreKey, vaultName)
29 }
30 if !reAzureSafeName.MatchString(secretName) {
31 return "", fmt.Errorf("resolving secret '%s': store '%s': invalid secret name '%s'", req.Original, req.StoreKey, secretName)
32 }
33
34 token, err := s.accessToken(ctx)
35 if err != nil {
36 return "", fmt.Errorf("resolving secret '%s': store '%s': %w", req.Original, req.StoreKey, err)
37 }
38
39 httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s.vault.azure.net/secrets/%s?api-version=7.4", vaultName, secretName), nil)
40 if err != nil {
41 return "", fmt.Errorf("resolving secret '%s': store '%s': creating request: %w", req.Original, req.StoreKey, err)
42 }
43 httpReq.Header.Set("Authorization", "Bearer "+token)
44
45 resp, err := s.runtime.apiClient.Do(httpReq)
46 if err != nil {
47 return "", fmt.Errorf("resolving secret '%s': store '%s': request failed: %w", req.Original, req.StoreKey, err)
48 }
49 defer resp.Body.Close()
50
51 body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
52 if err != nil {
53 return "", fmt.Errorf("resolving secret '%s': store '%s': reading response: %w", req.Original, req.StoreKey, err)
54 }
55 if resp.StatusCode != http.StatusOK {
56 return "", fmt.Errorf("resolving secret '%s': store '%s': Azure Key Vault returned HTTP %d: %s", req.Original, req.StoreKey, resp.StatusCode, httpx.TruncateBody(body))
57 }
58
59 var result struct {
60 Value string `json:"value"`
61 }
62 if err := json.Unmarshal(body, &result); err != nil {
63 return "", fmt.Errorf("resolving secret '%s': store '%s': parsing response: %w", req.Original, req.StoreKey, err)
64 }
65 if result.Value == "" {
66 return "", fmt.Errorf("resolving secret '%s': store '%s': Azure Key Vault returned empty secret value", req.Original, req.StoreKey)
67 }
68 logResolvedRequest(ctx, req, vaultName, secretName)
69 return result.Value, nil
70 }
71
72 func logResolvedRequest(ctx context.Context, req secretstore.ResolveRequest, vaultName, secretName string) {
73 if log, ok := logger.LoggerFromContext(ctx); ok {
74 log.Infof("resolved secret via azure-kv secretstore '%s' vault '%s' secret '%s'", req.StoreKey, vaultName, secretName)
75 }
76 }
77
78 func splitOperand(operand string) (string, string, bool) {
79 vaultName, secretName, ok := strings.Cut(operand, "/")
80 return vaultName, secretName, ok && vaultName != "" && secretName != ""
81 }
82
83 func (s *publishedStore) accessToken(ctx context.Context) (string, error) {
84 if s.tokenProvider == nil {
85 return "", fmt.Errorf("azure token provider is not initialized")
86 }
87
88 token, _, err := s.tokenProvider.Token(ctx)
89 if err != nil {
90 return "", fmt.Errorf("acquiring Azure Key Vault access token: %w", err)
91 }
92 return token, nil
93 }