diff --git a/go.mod b/go.mod index 7862575b0e..5bf2d28175 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 google.golang.org/protobuf v1.28.1 k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a - k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e + k8s.io/apimachinery v0.0.0-20221108055230-fd8a60496be5 k8s.io/klog/v2 v2.80.1 k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 k8s.io/utils v0.0.0-20221107191617-1a15be271d1d @@ -60,5 +60,5 @@ require ( replace ( k8s.io/api => k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e + k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20221108055230-fd8a60496be5 ) diff --git a/go.sum b/go.sum index a4b1eb4c43..b27927ed1b 100644 --- a/go.sum +++ b/go.sum @@ -478,8 +478,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a h1:GaCla9HtNyi63kysI/cyeA4bv6wRkIyuiUeXpaTF+dw= k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a/go.mod h1:PSXY9/fSNyKgKHUU+O9scnZiW8m+V1znqk49oI6hAEY= -k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e h1:zX/AC2CNrYwngyVHVHcsCL36uUtC/3tiZOE6/gfojvc= -k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e/go.mod h1:VXMmlsE7YRJ5vyAyWpkKIfFkEbDNpVs0ObpkuQf1WfM= +k8s.io/apimachinery v0.0.0-20221108055230-fd8a60496be5 h1:iFAMJ1evvrO6X7dS7EKujS6An+bp3u/dD6opu8rn0QA= +k8s.io/apimachinery v0.0.0-20221108055230-fd8a60496be5/go.mod h1:VXMmlsE7YRJ5vyAyWpkKIfFkEbDNpVs0ObpkuQf1WfM= k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= diff --git a/tools/clientcmd/api/helpers.go b/tools/clientcmd/api/helpers.go index 1b4fefdb44..dd5f918067 100644 --- a/tools/clientcmd/api/helpers.go +++ b/tools/clientcmd/api/helpers.go @@ -23,6 +23,8 @@ import ( "os" "path" "path/filepath" + "reflect" + "strings" ) func init() { @@ -81,21 +83,21 @@ func MinifyConfig(config *Config) error { } var ( - redactedBytes []byte dataOmittedBytes []byte + redactedBytes []byte ) -// Flatten redacts raw data entries from the config object for a human-readable view. +// ShortenConfig redacts raw data entries from the config object for a human-readable view. func ShortenConfig(config *Config) { - // trick json encoder into printing a human readable string in the raw data + // trick json encoder into printing a human-readable string in the raw data // by base64 decoding what we want to print. Relies on implementation of // http://golang.org/pkg/encoding/json/#Marshal using base64 to encode []byte for key, authInfo := range config.AuthInfos { if len(authInfo.ClientKeyData) > 0 { - authInfo.ClientKeyData = redactedBytes + authInfo.ClientKeyData = dataOmittedBytes } if len(authInfo.ClientCertificateData) > 0 { - authInfo.ClientCertificateData = redactedBytes + authInfo.ClientCertificateData = dataOmittedBytes } if len(authInfo.Token) > 0 { authInfo.Token = "REDACTED" @@ -110,7 +112,7 @@ func ShortenConfig(config *Config) { } } -// Flatten changes the config object into a self contained config (useful for making secrets) +// FlattenConfig changes the config object into a self-contained config (useful for making secrets) func FlattenConfig(config *Config) error { for key, authInfo := range config.AuthInfos { baseDir, err := MakeAbs(path.Dir(authInfo.LocationOfOrigin), "") @@ -188,3 +190,77 @@ func MakeAbs(path, base string) (string, error) { } return filepath.Join(base, path), nil } + +// RedactSecrets replaces any sensitive values with REDACTED +func RedactSecrets(config *Config) error { + return redactSecrets(reflect.ValueOf(config), false) +} + +func redactSecrets(curr reflect.Value, redact bool) error { + redactedBytes = []byte("REDACTED") + if !curr.IsValid() { + return nil + } + + actualCurrValue := curr + if curr.Kind() == reflect.Ptr { + actualCurrValue = curr.Elem() + } + + switch actualCurrValue.Kind() { + case reflect.Map: + for _, v := range actualCurrValue.MapKeys() { + err := redactSecrets(actualCurrValue.MapIndex(v), false) + if err != nil { + return err + } + } + return nil + + case reflect.String: + if redact { + if !actualCurrValue.IsZero() { + actualCurrValue.SetString("REDACTED") + } + } + return nil + + case reflect.Slice: + if actualCurrValue.Type() == reflect.TypeOf([]byte{}) && redact { + if !actualCurrValue.IsNil() { + actualCurrValue.SetBytes(redactedBytes) + } + return nil + } + for i := 0; i < actualCurrValue.Len(); i++ { + err := redactSecrets(actualCurrValue.Index(i), false) + if err != nil { + return err + } + } + return nil + + case reflect.Struct: + for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { + currFieldValue := actualCurrValue.Field(fieldIndex) + currFieldType := actualCurrValue.Type().Field(fieldIndex) + currYamlTag := currFieldType.Tag.Get("datapolicy") + currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0] + if currFieldTypeYamlName != "" { + err := redactSecrets(currFieldValue, true) + if err != nil { + return err + } + } else { + err := redactSecrets(currFieldValue, false) + if err != nil { + return err + } + } + } + return nil + + default: + return nil + } +} diff --git a/tools/clientcmd/api/helpers_test.go b/tools/clientcmd/api/helpers_test.go index 12aab343d8..169191f1ce 100644 --- a/tools/clientcmd/api/helpers_test.go +++ b/tools/clientcmd/api/helpers_test.go @@ -17,6 +17,7 @@ limitations under the License. package api import ( + "bytes" "fmt" "os" "reflect" @@ -240,8 +241,8 @@ func Example_minifyAndShorten() { // users: // red-user: // LocationOfOrigin: "" - // client-certificate-data: REDACTED - // client-key-data: REDACTED + // client-certificate-data: DATA+OMITTED + // client-key-data: DATA+OMITTED // token: REDACTED } @@ -274,7 +275,6 @@ func TestShortenSuccess(t *testing.T) { t.Errorf("expected %v, got %v", startingConfig.Contexts, mutatingConfig.Contexts) } - redacted := string(redactedBytes) dataOmitted := string(dataOmittedBytes) if len(mutatingConfig.Clusters) != 2 { t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters) @@ -292,13 +292,65 @@ func TestShortenSuccess(t *testing.T) { if !reflect.DeepEqual(startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) { t.Errorf("expected %v, got %v", startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) } - if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData) != redacted { - t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData)) + if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData) != dataOmitted { + t.Errorf("expected %v, got %v", dataOmitted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientCertificateData)) } - if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) != redacted { - t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData)) + if string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) != dataOmitted { + t.Errorf("expected %v, got %v", dataOmitted, string(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData)) + } + if mutatingConfig.AuthInfos[changingAuthInfo].Token != "REDACTED" { + t.Errorf("expected REDACTED, got %q", mutatingConfig.AuthInfos[changingAuthInfo].Token) + } +} + +func TestRedactSecrets(t *testing.T) { + certFile, _ := os.CreateTemp("", "") + defer os.Remove(certFile.Name()) + keyFile, _ := os.CreateTemp("", "") + defer os.Remove(keyFile.Name()) + caFile, _ := os.CreateTemp("", "") + defer os.Remove(caFile.Name()) + + certData := "cert" + keyData := "key" + caData := "ca" + + unchangingCluster := "chicken-cluster" + unchangingAuthInfo := "blue-user" + changingAuthInfo := "red-user" + + startingConfig := newMergedConfig(certFile.Name(), certData, keyFile.Name(), keyData, caFile.Name(), caData, t) + mutatingConfig := startingConfig + + err := RedactSecrets(&mutatingConfig) + if err != nil { + t.Errorf("unexpected error redacting secrets:\n%v", err) + } + + if len(mutatingConfig.Contexts) != 2 { + t.Errorf("unexpected contexts: %v", mutatingConfig.Contexts) + } + if !reflect.DeepEqual(startingConfig.Contexts, mutatingConfig.Contexts) { + t.Errorf("expected %v, got %v", startingConfig.Contexts, mutatingConfig.Contexts) + } + + if len(mutatingConfig.Clusters) != 2 { + t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters) + } + if !reflect.DeepEqual(startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) { + t.Errorf("expected %v, got %v", startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) + } + + if len(mutatingConfig.AuthInfos) != 2 { + t.Errorf("unexpected users: %v", mutatingConfig.AuthInfos) + } + if !reflect.DeepEqual(startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) { + t.Errorf("expected %v, got %v", startingConfig.AuthInfos[unchangingAuthInfo], mutatingConfig.AuthInfos[unchangingAuthInfo]) } if mutatingConfig.AuthInfos[changingAuthInfo].Token != "REDACTED" { t.Errorf("expected REDACTED, got %v", mutatingConfig.AuthInfos[changingAuthInfo].Token) } + if !bytes.Equal(mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData, []byte("REDACTED")) { + t.Errorf("expected REDACTED, got %s", mutatingConfig.AuthInfos[changingAuthInfo].ClientKeyData) + } }