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 }