Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug to query header search attributes correctly in visibility #6163

Merged
merged 5 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/definition/indexedKeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const (
// Attr is prefix of custom search attributes
Attr = "Attr"
// HeaderFormat is the format of context headers in search attributes
HeaderFormat = "Header.%s"
HeaderFormat = "Header_%s"
)

// defaultIndexedKeys defines all searchable keys
Expand Down
47 changes: 46 additions & 1 deletion common/elasticsearch/validator/queryValidator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/uber/cadence/common/definition"
"github.com/uber/cadence/common/dynamicconfig"
"github.com/uber/cadence/common/types"
)

func TestValidateQuery(t *testing.T) {
Expand All @@ -35,6 +36,7 @@ func TestValidateQuery(t *testing.T) {
query string
validated string
err string
dcValid map[string]interface{}
}{
{
msg: "empty query",
Expand Down Expand Up @@ -126,11 +128,54 @@ func TestValidateQuery(t *testing.T) {
query: "WorkflowID = 'wid' union select * from dummy",
err: "Invalid select query.",
},
{
msg: "valid custom search attribute",
query: "CustomStringField = 'value'",
validated: "`Attr.CustomStringField` = 'value'",
},
{
msg: "custom search attribute can contain underscore",
query: "Custom_Field = 'value'",
validated: "`Attr.Custom_Field` = 'value'",
dcValid: map[string]interface{}{
"Custom_Field": types.IndexedValueTypeString,
},
},
{
msg: "custom search attribute can contain number",
query: "Custom_0 = 'value'",
validated: "`Attr.Custom_0` = 'value'",
dcValid: map[string]interface{}{
"Custom_0": types.IndexedValueTypeString,
},
},
{
msg: "customg search attribute cannot contain dot",
query: "Custom.Field = 'value'",
err: "invalid search attribute \"Field\"",
dcValid: map[string]interface{}{
"Custom.Field": types.IndexedValueTypeString,
},
},
{
msg: "custom search attribute cannot contain dash",
query: "Custom-Field = 'value'",
err: "invalid comparison expression",
dcValid: map[string]interface{}{
"Custom-Field": types.IndexedValueTypeString,
},
},
}

for _, tt := range tests {
t.Run(tt.msg, func(t *testing.T) {
validSearchAttr := dynamicconfig.GetMapPropertyFn(definition.GetDefaultIndexedKeys())
validSearchAttr := func(opts ...dynamicconfig.FilterOption) map[string]interface{} {
valid := definition.GetDefaultIndexedKeys()
for k, v := range tt.dcValid {
valid[k] = v
}
return valid
}
validateSearchAttr := dynamicconfig.GetBoolPropertyFn(true)
qv := NewQueryValidator(validSearchAttr, validateSearchAttr)
validated, err := qv.ValidateQuery(tt.query)
Expand Down
52 changes: 52 additions & 0 deletions common/elasticsearch/validator/validate_search_attribute_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// The MIT License (MIT)

// Copyright (c) 2017-2020 Uber Technologies Inc.

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package validator

import (
"fmt"
"regexp"
"strings"
)

var (
validSearchAttributeKey = regexp.MustCompile(`^[a-zA-Z][a-zA-Z_0-9]*$`)
nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]+`)
)

// ValidateSearchAttributeKey checks if the search attribute key has valid format
func ValidateSearchAttributeKey(name string) error {
if !validSearchAttributeKey.MatchString(name) {
return fmt.Errorf("has to be contain alphanumeric and start with a letter")
}
return nil
}

// SanitizeSearchAttributeKey try to sanitize the search attribute key
func SanitizeSearchAttributeKey(name string) (string, error) {
sanitized := nonAlphanumericRegex.ReplaceAllString(name, "_")
// remove leading and trailing underscores
sanitized = strings.Trim(sanitized, "_")
// remove leading numbers
sanitized = strings.TrimLeft(sanitized, "0123456789")
return sanitized, ValidateSearchAttributeKey(sanitized)
}
120 changes: 120 additions & 0 deletions common/elasticsearch/validator/validate_search_attribute_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// The MIT License (MIT)

// Copyright (c) 2017-2020 Uber Technologies Inc.

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package validator

import (
"testing"
)

func TestValidateSearchAttributeKey(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "pure character",
args: args{name: "CustomStringField"},
},
{
name: "alphanumeric",
args: args{name: "CustomStringField1"},
},
{
name: "start with number",
args: args{name: "1CustomStringField"},
wantErr: true,
},
{
name: "contain special character",
args: args{name: "CustomStringField!"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateSearchAttributeKey(tt.args.name); (err != nil) != tt.wantErr {
t.Errorf("ValidateSearchAttributeKey() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestSanitizeSearchAttributeKey(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "pure character",
args: args{name: "CustomStringField"},
want: "CustomStringField",
},
{
name: "alphanumeric",
args: args{name: "CustomStringField1"},
want: "CustomStringField1",
},
{
name: "start with number",
args: args{name: "1CustomStringField"},
want: "CustomStringField",
},
{
name: "contain special character",
args: args{name: "CustomStringField!"},
want: "CustomStringField",
},
{
name: "contain special character in the middle",
args: args{name: "CustomString-Field"},
want: "CustomString_Field",
},
{
name: "all numbers",
args: args{name: "1234567890"},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SanitizeSearchAttributeKey(tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("SanitizeSearchAttributeKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("SanitizeSearchAttributeKey() = %v, want %v", got, tt.want)
}
})
}
}
8 changes: 4 additions & 4 deletions service/history/task/transfer_active_task_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,7 @@ func (s *transferActiveTaskExecutorSuite) TestProcessRecordWorkflowStartedTaskWi
s.mockShard.GetConfig().EnableContextHeaderInVisibility = func(domain string) bool { return true }
s.mockShard.GetConfig().ValidSearchAttributes = func(opts ...dc.FilterOption) map[string]interface{} {
return map[string]interface{}{
"Header.contextKey": struct{}{},
"Header_context_key": struct{}{},
}
}

Expand Down Expand Up @@ -1626,7 +1626,7 @@ func (s *transferActiveTaskExecutorSuite) TestProcessUpsertWorkflowSearchAttribu
s.mockShard.GetConfig().EnableContextHeaderInVisibility = func(domain string) bool { return true }
s.mockShard.GetConfig().ValidSearchAttributes = func(opts ...dc.FilterOption) map[string]interface{} {
return map[string]interface{}{
"Header.contextKey": struct{}{},
"Header_context_key": struct{}{},
}
}

Expand Down Expand Up @@ -1782,7 +1782,7 @@ func createRecordWorkflowExecutionStartedRequest(
t.Fatal(err)
}
searchAttributes = map[string][]byte{
"Header.contextKey": contextValueJSONString,
"Header_context_key": contextValueJSONString,
}
}
return &persistence.RecordWorkflowExecutionStartedRequest{
Expand Down Expand Up @@ -1937,7 +1937,7 @@ func createUpsertWorkflowSearchAttributesRequest(
t.Fatal(err)
}
searchAttributes = map[string][]byte{
"Header.contextKey": contextValueJSONString,
"Header_context_key": contextValueJSONString,
}
}

Expand Down
4 changes: 2 additions & 2 deletions service/history/task/transfer_standby_task_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,7 @@ func (s *transferStandbyTaskExecutorSuite) TestProcessRecordWorkflowStartedTaskW
}
s.mockShard.GetConfig().ValidSearchAttributes = func(opts ...dc.FilterOption) map[string]interface{} {
return map[string]interface{}{
"Header.contextKey": struct{}{},
"Header_context_key": struct{}{},
}
}

Expand Down Expand Up @@ -906,7 +906,7 @@ func (s *transferStandbyTaskExecutorSuite) TestProcessUpsertWorkflowSearchAttrib
}
s.mockShard.GetConfig().ValidSearchAttributes = func(opts ...dc.FilterOption) map[string]interface{} {
return map[string]interface{}{
"Header.contextKey": struct{}{},
"Header_context_key": struct{}{},
}
}

Expand Down
8 changes: 7 additions & 1 deletion service/history/task/transfer_task_executor_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/uber/cadence/common"
"github.com/uber/cadence/common/backoff"
"github.com/uber/cadence/common/definition"
"github.com/uber/cadence/common/elasticsearch/validator"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elasticseach is a plugin. rest of the codebase shouldn't directly depend on this package. same search attribute constrationts apply to Pinot and other future visibility stores as well. so let's move this validator to common/visibility or somewhere generic like that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"github.com/uber/cadence/common/log"
"github.com/uber/cadence/common/log/tag"
"github.com/uber/cadence/common/metrics"
Expand Down Expand Up @@ -402,7 +403,12 @@ func getWorkflowMemo(

func appendContextHeaderToSearchAttributes(attr, context map[string][]byte, allowedKeys map[string]interface{}) (map[string][]byte, error) {
for k, v := range context {
key := fmt.Sprintf(definition.HeaderFormat, k)
unsanitizedKey := fmt.Sprintf(definition.HeaderFormat, k)
key, err := validator.SanitizeSearchAttributeKey(unsanitizedKey)
if err != nil {
return nil, fmt.Errorf("fail to sanitize context key %s: %w", key, err)
}

if _, ok := attr[key]; ok { // skip if key already exists
continue
}
Expand Down
4 changes: 2 additions & 2 deletions service/history/testing/workflow_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ func StartWorkflow(
ExecutionStartToCloseTimeoutSeconds: common.Int32Ptr(2),
TaskStartToCloseTimeoutSeconds: common.Int32Ptr(1),
Header: &types.Header{Fields: map[string][]byte{
"contextKey": []byte("contextValue"),
"invalidContextKey": []byte("invalidContextValue"),
"context-key": []byte("contextValue"),
"invalid-context-key": []byte("invalidContextValue"),
}},
},
PartitionConfig: map[string]string{"userid": uuid.New()},
Expand Down
12 changes: 2 additions & 10 deletions tools/cli/admin_cluster_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,24 @@ package cli
import (
"encoding/json"
"fmt"
"regexp"

"github.com/fatih/color"
"github.com/pborman/uuid"
"github.com/urfave/cli"

"github.com/uber/cadence/common"
"github.com/uber/cadence/common/elasticsearch/validator"
"github.com/uber/cadence/common/types"
"github.com/uber/cadence/service/worker/failovermanager"
)

// An indirection for the prompt function so that it can be mocked in the unit tests
var promptFn = prompt
var validSearchAttributeKey = regexp.MustCompile(`^[a-zA-Z][a-zA-Z_.-0-9]*$`)

// AdminAddSearchAttribute to whitelist search attribute
func AdminAddSearchAttribute(c *cli.Context) {
key := getRequiredOption(c, FlagSearchAttributesKey)
if err := validateSearchAttributeKey(key); err != nil {
if err := validator.ValidateSearchAttributeKey(key); err != nil {
ErrorAndExit("Invalid search-attribute key.", err)
}

Expand Down Expand Up @@ -158,13 +157,6 @@ func intValTypeToString(valType int) string {
}
}

func validateSearchAttributeKey(name string) error {
if !validSearchAttributeKey.MatchString(name) {
return fmt.Errorf("has to be contain alphanumeric and start with a letter")
}
return nil
}

func isValueTypeValid(valType int) bool {
return valType >= 0 && valType <= 5
}
Loading
Loading