Files
2026-04-28 22:29:50 +03:00

133 lines
3.0 KiB
Go

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)
}