133 lines
3.0 KiB
Go
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)
|
|
}
|