diff --git a/api/core/v3/entity_config.go b/api/core/v3/entity_config.go index 1549dd4313..03e5e6f2b5 100644 --- a/api/core/v3/entity_config.go +++ b/api/core/v3/entity_config.go @@ -5,6 +5,7 @@ import ( "strings" corev2 "github.com/sensu/sensu-go/api/core/v2" + stringutil "github.com/sensu/sensu-go/api/core/v3/internal/strings" ) var entityConfigRBACName = (&corev2.Entity{}).RBACName() @@ -31,3 +32,35 @@ func MergeMapWithPrefix(a map[string]string, b map[string]string, prefix string) a[prefix+k] = v } } + +func redactMap(m map[string]string, redact []string) map[string]string { + if len(redact) == 0 { + redact = corev2.DefaultRedactFields + } + result := make(map[string]string, len(m)) + for k, v := range m { + if stringutil.FoundInArray(k, redact) { + result[k] = corev2.Redacted + } else { + result[k] = v + } + } + return result +} + +// ProduceRedacted redacts the entity according to the entity's Redact fields. +// A redacted copy is returned. The copy contains pointers to the original's +// memory, with different Labels and Annotations. +func (e *EntityConfig) ProduceRedacted() Resource { + if e == nil { + return nil + } + if e.Metadata == nil || (e.Metadata.Labels == nil && e.Metadata.Annotations == nil) { + return e + } + copy := &EntityConfig{} + *copy = *e + copy.Metadata.Annotations = redactMap(e.Metadata.Annotations, e.Redact) + copy.Metadata.Labels = redactMap(e.Metadata.Labels, e.Redact) + return copy +} diff --git a/api/core/v3/entity_config_test.go b/api/core/v3/entity_config_test.go index 568ce1508a..fbc00653ad 100644 --- a/api/core/v3/entity_config_test.go +++ b/api/core/v3/entity_config_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - v2 "github.com/sensu/sensu-go/api/core/v2" + corev2 "github.com/sensu/sensu-go/api/core/v2" ) func TestEntityConfigFields(t *testing.T) { @@ -22,26 +22,26 @@ func TestEntityConfigFields(t *testing.T) { }, { name: "exposes deregister", - args: &EntityConfig{Metadata: &v2.ObjectMeta{}, Deregister: true}, + args: &EntityConfig{Metadata: &corev2.ObjectMeta{}, Deregister: true}, wantKey: "entity_config.deregister", want: "true", }, { name: "exposes class", - args: &EntityConfig{Metadata: &v2.ObjectMeta{}, EntityClass: "agent"}, + args: &EntityConfig{Metadata: &corev2.ObjectMeta{}, EntityClass: "agent"}, wantKey: "entity_config.entity_class", want: "agent", }, { name: "exposes subscriptions", - args: &EntityConfig{Metadata: &v2.ObjectMeta{}, Subscriptions: []string{"www", "unix"}}, + args: &EntityConfig{Metadata: &corev2.ObjectMeta{}, Subscriptions: []string{"www", "unix"}}, wantKey: "entity_config.subscriptions", want: "www,unix", }, { name: "exposes labels", args: &EntityConfig{ - Metadata: &v2.ObjectMeta{ + Metadata: &corev2.ObjectMeta{ Labels: map[string]string{"region": "philadelphia"}, }, }, @@ -58,3 +58,78 @@ func TestEntityConfigFields(t *testing.T) { }) } } + +func TestEntityConfig_ProduceRedacted(t *testing.T) { + tests := []struct { + name string + in *EntityConfig + want *EntityConfig + }{ + { + name: "nil metadata", + in: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Metadata = nil + return cfg + }(), + want: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Metadata = nil + return cfg + }(), + }, + { + name: "nothing to redact", + in: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Metadata.Labels["my_field"] = "test123" + return cfg + }(), + want: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Metadata.Labels["my_field"] = "test123" + return cfg + }(), + }, + { + name: "redact default fields", + in: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Metadata.Labels["my_field"] = "test123" + cfg.Metadata.Labels["password"] = "test123" + return cfg + }(), + want: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Metadata.Labels["my_field"] = "test123" + cfg.Metadata.Labels["password"] = corev2.Redacted + return cfg + }(), + }, + { + name: "redact custom fields", + in: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Redact = []string{"my_field"} + cfg.Metadata.Labels["my_field"] = "test123" + cfg.Metadata.Labels["password"] = "test123" + return cfg + }(), + want: func() *EntityConfig { + cfg := FixtureEntityConfig("test") + cfg.Redact = []string{"my_field"} + cfg.Metadata.Labels["my_field"] = corev2.Redacted + cfg.Metadata.Labels["password"] = "test123" + return cfg + }(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.in + if got := e.ProduceRedacted(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("EntityConfig.ProduceRedacted() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/api/core/v3/internal/strings/strings.go b/api/core/v3/internal/strings/strings.go new file mode 100644 index 0000000000..400bb517eb --- /dev/null +++ b/api/core/v3/internal/strings/strings.go @@ -0,0 +1,60 @@ +package strings + +import ( + "strings" + "unicode" +) + +const ( + lowerAlphaStart = 97 + lowerAlphaStop = 122 +) + +func isAlpha(r rune) bool { + return r >= lowerAlphaStart && r <= lowerAlphaStop +} + +func alphaNumeric(s string) bool { + for _, r := range s { + if !(unicode.IsDigit(r) || isAlpha(r)) { + return false + } + } + return true +} + +func normalize(s string) string { + if alphaNumeric(s) { + return s + } + lowered := strings.ToLower(s) + if alphaNumeric(lowered) { + return lowered + } + trimmed := make([]rune, 0, len(lowered)) + for _, r := range lowered { + if isAlpha(r) { + trimmed = append(trimmed, r) + } + } + return string(trimmed) +} + +// FoundInArray searches array for item without distinguishing between uppercase +// and lowercase and non-alphanumeric characters. Returns true if item is a +// value of array +func FoundInArray(item string, array []string) bool { + if item == "" || len(array) == 0 { + return false + } + + item = normalize(item) + + for i := range array { + if normalize(array[i]) == item { + return true + } + } + + return false +} diff --git a/api/core/v3/internal/strings/strings_test.go b/api/core/v3/internal/strings/strings_test.go new file mode 100644 index 0000000000..d1dfc90c10 --- /dev/null +++ b/api/core/v3/internal/strings/strings_test.go @@ -0,0 +1,34 @@ +package strings + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFoundInArray(t *testing.T) { + var array []string + + found := FoundInArray("Foo", []string{}) + assert.False(t, found) + + array = []string{"foo", "bar"} + found = FoundInArray("Foo", array) + assert.True(t, found) + + array = []string{"foo", "bar"} + found = FoundInArray("FooBar", array) + assert.False(t, found) + + array = []string{"foo", "bar"} + found = FoundInArray("Foo ", array) + assert.True(t, found) + + array = []string{"foo_bar"} + found = FoundInArray("Foo_Bar", array) + assert.True(t, found) + + array = []string{"foobar"} + found = FoundInArray("Foo_Qux", array) + assert.False(t, found) +} diff --git a/api/core/v3/redacter.go b/api/core/v3/redacter.go new file mode 100644 index 0000000000..38d781aedb --- /dev/null +++ b/api/core/v3/redacter.go @@ -0,0 +1,7 @@ +package v3 + +// Redacter can return a redacted copy of the resource +type Redacter interface { + // ProduceRedacted returns a redacted copy of the resource + ProduceRedacted() Resource +} diff --git a/backend/api/generic.go b/backend/api/generic.go index 5a8e03c122..be9ca4129f 100644 --- a/backend/api/generic.go +++ b/backend/api/generic.go @@ -155,7 +155,13 @@ func (g *GenericClient) getResource(ctx context.Context, name string, value core if err != nil { return err } - return wrapper.UnwrapInto(value) + if err := wrapper.UnwrapInto(value); err != nil { + return err + } + if redacter, ok := value.(corev3.Redacter); ok { + value = redacter.ProduceRedacted() + } + return err } // Get gets a resource, if authorized @@ -184,7 +190,16 @@ func (g *GenericClient) list(ctx context.Context, resources interface{}, pred *s if err != nil { return err } - return list.UnwrapInto(resources) + + if err := list.UnwrapInto(resources); err != nil { + return err + } + if redacters, ok := resources.([]corev3.Redacter); ok { + for i, redacter := range redacters { + redacters[i] = redacter.ProduceRedacted() + } + } + return nil } // List lists all resources within a namespace, according to a selection diff --git a/backend/api/generic_test.go b/backend/api/generic_test.go index 9ad3a76d84..7db5f0b14a 100644 --- a/backend/api/generic_test.go +++ b/backend/api/generic_test.go @@ -415,7 +415,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", ResourceName: "default", UserName: "tom", Verb: "get", @@ -424,7 +424,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", UserName: "tom", Verb: "list", }: true, @@ -450,7 +450,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", ResourceName: "default", UserName: "tom", Verb: "get", @@ -459,7 +459,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", ResourceName: "default", UserName: "tom", Verb: "create", @@ -468,7 +468,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", ResourceName: "default", UserName: "tom", Verb: "delete", @@ -477,7 +477,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", ResourceName: "default", UserName: "tom", Verb: "update", @@ -486,7 +486,7 @@ func TestGenericClientStoreV2(t *testing.T) { APIGroup: "core", APIVersion: "v3", Namespace: "default", - Resource: "entity_configs", + Resource: "entities", UserName: "tom", Verb: "list", }: true, @@ -563,6 +563,100 @@ func TestGenericClientStoreV2(t *testing.T) { } } +func TestGenericClientStoreV2_sensu_enterprise_go_GH2484(t *testing.T) { + makeStore := func(entity *corev3.EntityConfig) storev2.Interface { + store := new(storetest.Store) + if entity == nil { + entity = corev3.FixtureEntityConfig("default") + entity.Redact = []string{"password"} + entity.Metadata.Labels["password"] = "test" + entity.Metadata.Labels["my_label"] = "test" + } + wrappedResource, err := storev2.WrapResource(entity) + if err != nil { + panic(err) + } + store.On("Get", mock.Anything).Return(wrappedResource, nil) + store.On("List", mock.Anything, mock.Anything).Return(wrap.List{wrappedResource.(*wrap.Wrapper)}, nil) + return store + } + v3AllAccess := func() authorization.Authorizer { + return &mockAuth{ + attrs: map[authorization.AttributesKey]bool{ + { + APIGroup: "core", + APIVersion: "v3", + Namespace: "default", + Resource: "entities", + ResourceName: "default", + UserName: "tom", + Verb: "get", + }: true, + { + APIGroup: "core", + APIVersion: "v3", + Namespace: "default", + Resource: "entities", + ResourceName: "default", + UserName: "tom", + Verb: "create", + }: true, + { + APIGroup: "core", + APIVersion: "v3", + Namespace: "default", + Resource: "entities", + ResourceName: "default", + UserName: "tom", + Verb: "delete", + }: true, + { + APIGroup: "core", + APIVersion: "v3", + Namespace: "default", + Resource: "entities", + ResourceName: "default", + UserName: "tom", + Verb: "update", + }: true, + { + APIGroup: "core", + APIVersion: "v3", + Namespace: "default", + Resource: "entities", + UserName: "tom", + Verb: "list", + }: true, + }, + } + } + + ctx := contextWithUser(defaultContext(), "tom", nil) + client := defaultV2TestClient(makeStore(nil), v3AllAccess()) + listVal := []corev2.Resource{} + if err := client.List(ctx, &listVal, &store.SelectionPredicate{}); err != nil { + t.Fatal(err) + } + if listVal[0].GetObjectMeta().Labels["password"] != corev2.Redacted { + t.Errorf("Labels['password'] = %s, got: %s", corev2.Redacted, listVal[0].GetObjectMeta().Labels["password"]) + } + if listVal[0].GetObjectMeta().Labels["my_label"] != "test" { + t.Errorf("Labels['my_label'] = %s, got: %s", "test", listVal[0].GetObjectMeta().Labels["my_label"]) + } + + client = defaultV2TestClient(makeStore(nil), v3AllAccess()) + getVal := corev3.V2ResourceProxy{Resource: &corev3.EntityConfig{}} + if err := client.Get(ctx, "default", &getVal); err != nil { + t.Fatal(err) + } + if getVal.GetObjectMeta().Labels["password"] != corev2.Redacted { + t.Errorf("Labels['password'] = %s, got: %s", corev2.Redacted, getVal.GetObjectMeta().Labels["password"]) + } + if getVal.GetObjectMeta().Labels["my_label"] != "test" { + t.Errorf("Labels['my_label'] = %s, got: %s", "test", getVal.GetObjectMeta().Labels["my_label"]) + } +} + func TestSetTypeMetaV3Resource(t *testing.T) { client := &GenericClient{} err := client.SetTypeMeta(corev2.TypeMeta{