github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/secrets/tenant.go (about)

     1  package secrets
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/patrickmn/go-cache"
    10  	"github.com/rs/zerolog"
    11  
    12  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    13  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    14  )
    15  
    16  type SecretProvider interface {
    17  	GetSecretCredentials(ctx context.Context, tenantID model.GlobalID) (*sm.SecretStore, error)
    18  	GetSecretValue(ctx context.Context, tenantID model.GlobalID, secretKey string) (string, error)
    19  	IsProtocolSecretsEnabled() bool
    20  }
    21  
    22  type TenantProvider interface {
    23  	GetTenant(context.Context, *sm.TenantInfo) (*sm.Tenant, error)
    24  }
    25  
    26  // TenantSecrets provides backward compatibility with existing code
    27  type TenantSecrets struct {
    28  	tp                    TenantProvider
    29  	logger                zerolog.Logger
    30  	enableProtocolSecrets bool
    31  }
    32  
    33  // NewTenantSecrets creates a new TenantSecrets instance for backward compatibility
    34  func NewTenantSecrets(tp TenantProvider, logger zerolog.Logger) *TenantSecrets {
    35  	return &TenantSecrets{
    36  		tp:                    tp,
    37  		logger:                logger,
    38  		enableProtocolSecrets: false, // Default to false for backward compatibility
    39  	}
    40  }
    41  
    42  // GetSecretCredentials gets the secret store configuration for a tenant (backward compatibility)
    43  func (ts *TenantSecrets) GetSecretCredentials(ctx context.Context, tenantID model.GlobalID) (*sm.SecretStore, error) {
    44  	localTenantID, regionID := model.GetLocalAndRegionIDs(tenantID)
    45  	ts.logger.Debug().
    46  		Int("regionID", regionID).
    47  		Int64("tenantId", localTenantID).
    48  		Int64("globalTenantID", int64(tenantID)).
    49  		Msg("getting secret credentials")
    50  
    51  	tenant, err := ts.tp.GetTenant(ctx, &sm.TenantInfo{
    52  		Id: int64(tenantID),
    53  	})
    54  	if err != nil {
    55  		ts.logger.Warn().Err(err).Int64("tenantId", int64(tenantID)).Msg("failed to get tenant")
    56  		return nil, err
    57  	}
    58  
    59  	ts.logger.Debug().
    60  		Int64("tenantId", localTenantID).
    61  		Bool("tenantHasSecretStore", tenant.SecretStore != nil).
    62  		Msg("tenant retrieved for secret credentials")
    63  
    64  	if tenant.SecretStore != nil {
    65  		ts.logger.Debug().
    66  			Int64("tenantId", localTenantID).
    67  			Str("secretStoreUrl", tenant.SecretStore.Url).
    68  			Bool("hasSecretStoreToken", tenant.SecretStore.Token != "").
    69  			Float64("secretStoreExpiry", tenant.SecretStore.Expiry).
    70  			Msg("secret store configuration retrieved successfully")
    71  	}
    72  
    73  	return tenant.SecretStore, nil
    74  }
    75  
    76  // GetSecretValue implements SecretProvider interface (backward compatibility)
    77  func (ts *TenantSecrets) GetSecretValue(ctx context.Context, tenantID model.GlobalID, secretKey string) (string, error) {
    78  	// For backward compatibility, return empty string
    79  	// This will be replaced by the full implementation in PR 2
    80  	return "", nil
    81  }
    82  
    83  // IsProtocolSecretsEnabled returns whether protocol secrets are enabled for this probe
    84  func (ts *TenantSecrets) IsProtocolSecretsEnabled() bool {
    85  	return ts.enableProtocolSecrets
    86  }
    87  
    88  // UpdateCapabilities updates the probe capabilities
    89  func (ts *TenantSecrets) UpdateCapabilities(probeCapabilities *sm.Probe_Capabilities) {
    90  	ts.enableProtocolSecrets = false
    91  	if probeCapabilities != nil {
    92  		ts.enableProtocolSecrets = probeCapabilities.EnableProtocolSecrets
    93  	}
    94  }
    95  
    96  // secretProvider provides caching for secret values with TTL and intelligent response handling
    97  type secretProvider struct {
    98  	tenantProvider        TenantProvider
    99  	cache                 *cache.Cache
   100  	logger                zerolog.Logger
   101  	enableProtocolSecrets bool
   102  	gsmClientFactory      *GSMClientFactory
   103  }
   104  
   105  // NewSecretProvider creates a new secret provider
   106  func NewSecretProvider(tenantProvider TenantProvider, ttl time.Duration, logger zerolog.Logger) SecretProvider {
   107  	// go-cache handles cleanup automatically, so we don't need manual cleanup
   108  	// The cleanup interval is set to ttl/10 to ensure expired items are cleaned up reasonably quickly
   109  	cleanupInterval := ttl / 10
   110  	if cleanupInterval < time.Minute {
   111  		cleanupInterval = time.Minute
   112  	}
   113  
   114  	return &secretProvider{
   115  		tenantProvider:        tenantProvider,
   116  		cache:                 cache.New(ttl, cleanupInterval),
   117  		logger:                logger.With().Str("component", "secret-cache").Logger(),
   118  		enableProtocolSecrets: false, // Default to false
   119  		gsmClientFactory:      NewGSMClientFactory(),
   120  	}
   121  }
   122  
   123  // NewSecretProviderWithCapabilities creates a new secret provider with probe capabilities
   124  func NewSecretProviderWithCapabilities(tenantProvider TenantProvider, ttl time.Duration, logger zerolog.Logger, probeCapabilities *sm.Probe_Capabilities) SecretProvider {
   125  	enableProtocolSecrets := false
   126  	if probeCapabilities != nil {
   127  		enableProtocolSecrets = probeCapabilities.EnableProtocolSecrets
   128  	}
   129  
   130  	// go-cache handles cleanup automatically, so we don't need manual cleanup
   131  	// The cleanup interval is set to ttl/10 to ensure expired items are cleaned up reasonably quickly
   132  	cleanupInterval := ttl / 10
   133  	if cleanupInterval < time.Minute {
   134  		cleanupInterval = time.Minute
   135  	}
   136  
   137  	return &secretProvider{
   138  		tenantProvider:        tenantProvider,
   139  		cache:                 cache.New(ttl, cleanupInterval),
   140  		logger:                logger.With().Str("component", "secret-cache").Logger(),
   141  		enableProtocolSecrets: enableProtocolSecrets,
   142  		gsmClientFactory:      NewGSMClientFactory(),
   143  	}
   144  }
   145  
   146  // Close gracefully shuts down the secret provider
   147  func (sp *secretProvider) Close() {
   148  	// go-cache doesn't require explicit cleanup, but we can flush the cache
   149  	sp.cache.Flush()
   150  }
   151  
   152  // cacheKey creates a unique key for tenant+secret combination
   153  func (sp *secretProvider) cacheKey(tenantID model.GlobalID, secretKey string) string {
   154  	return fmt.Sprintf("%d:%s", tenantID, secretKey)
   155  }
   156  
   157  // GetSecretCredentials gets the secret store configuration for a tenant
   158  func (sp *secretProvider) GetSecretCredentials(ctx context.Context, tenantID model.GlobalID) (*sm.SecretStore, error) {
   159  	if sp.logger.GetLevel() <= zerolog.DebugLevel {
   160  		tenantID, regionID := model.GetLocalAndRegionIDs(tenantID)
   161  		sp.logger.Debug().Int("regionID", regionID).Int64("tenantId", tenantID).Msg("getting secret credentials")
   162  	}
   163  
   164  	tenant, err := sp.tenantProvider.GetTenant(ctx, &sm.TenantInfo{
   165  		Id: int64(tenantID),
   166  	})
   167  	if err != nil {
   168  		sp.logger.Warn().Err(err).Int64("tenantId", int64(tenantID)).Msg("failed to get tenant")
   169  		return nil, err
   170  	}
   171  
   172  	return tenant.SecretStore, nil
   173  }
   174  
   175  // GetSecretValue implements caching with intelligent GSM response handling
   176  func (sp *secretProvider) GetSecretValue(ctx context.Context, tenantID model.GlobalID, secretKey string) (string, error) {
   177  	cacheKey := sp.cacheKey(tenantID, secretKey)
   178  
   179  	// Check cache first
   180  	if cachedValue, found := sp.cache.Get(cacheKey); found {
   181  		sp.logger.Debug().
   182  			Int64("tenantId", int64(tenantID)).
   183  			Str("secretKey", secretKey).
   184  			Msg("secret cache hit")
   185  
   186  		return cachedValue.(string), nil
   187  	}
   188  
   189  	sp.logger.Debug().
   190  		Int64("tenantId", int64(tenantID)).
   191  		Str("secretKey", secretKey).
   192  		Msg("secret cache miss, fetching from GSM")
   193  
   194  	if sp.logger.GetLevel() <= zerolog.DebugLevel {
   195  		tenantID, regionID := model.GetLocalAndRegionIDs(tenantID)
   196  		sp.logger.Debug().Int("regionID", regionID).Int64("tenantId", tenantID).Str("secretKey", secretKey).Msg("getting secret value")
   197  	}
   198  
   199  	// Get the secret store configuration for this tenant
   200  	secretStore, err := sp.GetSecretCredentials(ctx, tenantID)
   201  	if err != nil {
   202  		return "", fmt.Errorf("failed to get secret store credentials: %w", err)
   203  	}
   204  
   205  	if secretStore == nil {
   206  		return "", fmt.Errorf("no secret store configured for tenant %d", tenantID)
   207  	}
   208  
   209  	// Create GSM client
   210  	client, err := sp.gsmClientFactory.CreateClient(secretStore.Url, secretStore.Token)
   211  	if err != nil {
   212  		return "", fmt.Errorf("failed to create GSM client: %w", err)
   213  	}
   214  
   215  	// Get the decrypted secret value
   216  	resp, err := client.DecryptSecretByIdWithResponse(ctx, secretKey)
   217  	if err != nil {
   218  		// Network error or client error - leave cache unchanged
   219  		sp.logger.Warn().
   220  			Err(err).
   221  			Int64("tenantId", int64(tenantID)).
   222  			Str("secretKey", secretKey).
   223  			Msg("network error fetching secret, leaving cache unchanged")
   224  
   225  		return "", fmt.Errorf("failed to contact GSM for secret '%s': %w", secretKey, err)
   226  	}
   227  
   228  	// Handle different status codes
   229  	switch resp.StatusCode() {
   230  	case http.StatusOK:
   231  		// Success - update cache and return value
   232  		if resp.JSON200 == nil {
   233  			return "", fmt.Errorf("empty response from GSM for secret %s", secretKey)
   234  		}
   235  
   236  		secretValue := resp.JSON200.Plaintext
   237  		sp.cache.Set(cacheKey, secretValue, cache.DefaultExpiration)
   238  
   239  		sp.logger.Debug().
   240  			Int64("tenantId", int64(tenantID)).
   241  			Str("secretKey", secretKey).
   242  			Msg("secret fetched from GSM and cached")
   243  
   244  		return secretValue, nil
   245  
   246  	case http.StatusNotFound:
   247  		// Secret not found - remove from cache
   248  		sp.cache.Delete(cacheKey)
   249  
   250  		sp.logger.Warn().
   251  			Int64("tenantId", int64(tenantID)).
   252  			Str("secretKey", secretKey).
   253  			Msg("secret not found in GSM, removed from cache")
   254  
   255  		return "", fmt.Errorf("secret '%s' not found in GSM (404)", secretKey)
   256  
   257  	case http.StatusUnauthorized:
   258  		// Auth issue - remove from cache (credentials may have changed)
   259  		sp.cache.Delete(cacheKey)
   260  
   261  		sp.logger.Warn().
   262  			Int64("tenantId", int64(tenantID)).
   263  			Str("secretKey", secretKey).
   264  			Msg("unauthorized accessing secret in GSM, removed from cache")
   265  
   266  		return "", fmt.Errorf("unauthorized to access secret '%s' in GSM (401)", secretKey)
   267  
   268  	default:
   269  		// 5xx or other errors - leave cache unchanged
   270  		statusCode := resp.StatusCode()
   271  
   272  		sp.logger.Warn().
   273  			Int("statusCode", statusCode).
   274  			Int64("tenantId", int64(tenantID)).
   275  			Str("secretKey", secretKey).
   276  			Msg("GSM returned error status, leaving cache unchanged")
   277  
   278  		return "", fmt.Errorf("GSM returned status %d for secret '%s'", statusCode, secretKey)
   279  	}
   280  }
   281  
   282  // IsProtocolSecretsEnabled returns whether protocol secrets are enabled for this probe
   283  func (sp *secretProvider) IsProtocolSecretsEnabled() bool {
   284  	return sp.enableProtocolSecrets
   285  }
   286  
   287  // UpdateCapabilities updates the probe capabilities
   288  func (sp *secretProvider) UpdateCapabilities(probeCapabilities *sm.Probe_Capabilities) {
   289  	sp.enableProtocolSecrets = false
   290  	if probeCapabilities != nil {
   291  		sp.enableProtocolSecrets = probeCapabilities.EnableProtocolSecrets
   292  	}
   293  }