package secrets import ( "encoding/json" "errors" "fmt" "slices" "sort" "strings" ) var ( ErrPlaintextResourceCredentials = errors.New("plaintext resource credentials are not allowed in metadata in production") ErrMissingResourceSecretRef = errors.New("secret_ref is required for this resource protocol in production") ) var credentialKeyFragments = []string{ "accesstoken", "clientsecret", "credential", "credentials", "domain", "password", "privatekey", "refreshtoken", "secret", "secrets", "token", "user", "username", } var safeReferenceKeys = []string{ "certificateverificationmode", "renderqualityprofile", "secretref", "secretreference", "vaultref", } func ValidateResourceSecretReadiness(protocol string, secretRef *string, metadata json.RawMessage, appEnv string) error { if !IsProductionEnv(appEnv) { return nil } paths, err := PlaintextCredentialMetadataPaths(metadata) if err != nil { return err } if len(paths) > 0 { return fmt.Errorf("%w: %s", ErrPlaintextResourceCredentials, strings.Join(paths, ", ")) } if ResourceProtocolRequiresSecretRef(protocol) && (secretRef == nil || strings.TrimSpace(*secretRef) == "") { return ErrMissingResourceSecretRef } return nil } func IsProductionEnv(appEnv string) bool { switch strings.ToLower(strings.TrimSpace(appEnv)) { case "prod", "production": return true default: return false } } func ResourceProtocolRequiresSecretRef(protocol string) bool { switch strings.ToLower(strings.TrimSpace(protocol)) { case "rdp", "vnc", "ssh": return true default: return false } } func PlaintextCredentialMetadataPaths(raw json.RawMessage) ([]string, error) { if len(raw) == 0 { return nil, nil } var value any if err := json.Unmarshal(raw, &value); err != nil { return nil, errors.New("metadata must be valid json") } metadata, ok := value.(map[string]any) if !ok { return nil, errors.New("metadata must be a json object") } var paths []string collectCredentialPaths(metadata, "", &paths) sort.Strings(paths) return slices.Compact(paths), nil } func collectCredentialPaths(value any, prefix string, paths *[]string) { switch typed := value.(type) { case map[string]any: for key, child := range typed { path := key if prefix != "" { path = prefix + "." + key } if isCredentialMetadataKey(key) { *paths = append(*paths, path) } collectCredentialPaths(child, path, paths) } case []any: for index, child := range typed { collectCredentialPaths(child, fmt.Sprintf("%s[%d]", prefix, index), paths) } } } func isCredentialMetadataKey(key string) bool { normalized := normalizeMetadataKey(key) if slices.Contains(safeReferenceKeys, normalized) { return false } for _, fragment := range credentialKeyFragments { if normalized == fragment || strings.HasSuffix(normalized, fragment) { return true } } return false } func normalizeMetadataKey(key string) string { key = strings.ToLower(strings.TrimSpace(key)) replacer := strings.NewReplacer("_", "", "-", "", " ", "", ".", "") return replacer.Replace(key) }