Skip to content

Commit

Permalink
Support jsonb search (#127)
Browse files Browse the repository at this point in the history
* Support jsonb search

Signed-off-by: clyang82 <[email protected]>

* Add 2 more test cases

Signed-off-by: clyang82 <[email protected]>

---------

Signed-off-by: clyang82 <[email protected]>
  • Loading branch information
clyang82 authored Jun 18, 2024
1 parent b8eb4d7 commit 354826f
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 2 deletions.
78 changes: 76 additions & 2 deletions pkg/services/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ package services

import (
"context"
"encoding/json"
e "errors"
"fmt"
"reflect"
"strings"

"gorm.io/gorm"

"github.com/Masterminds/squirrel"
"github.com/yaacov/tree-search-language/pkg/tsl"
"github.com/yaacov/tree-search-language/pkg/walkers/ident"
sqlFilter "github.com/yaacov/tree-search-language/pkg/walkers/sql"
"gorm.io/gorm"
"k8s.io/apimachinery/pkg/util/validation"

"github.com/openshift-online/maestro/pkg/api"
"github.com/openshift-online/maestro/pkg/dao"
Expand Down Expand Up @@ -154,6 +155,16 @@ func (s *sqlGenericService) buildSearch(listCtx *listContext, d *dao.GenericDao)
return true, nil
}

if isJSONBSearch(listCtx.args.Search) {
// focus on jsonb, ignore type = 'Single' or 'Bundle'
jsonbSearch, _, _ := strings.Cut(listCtx.args.Search, "and type=")
if err := validateJSONBSearch(jsonbSearch); err != nil {
return false, errors.BadRequest("failed to validate search query: %v", err)
}
(*d).Where(listCtx.args.Search, nil)
return true, nil
}

// create the TSL tree
tslTree, err := tsl.ParseTSL(listCtx.args.Search)
if err != nil {
Expand Down Expand Up @@ -333,3 +344,66 @@ func (s *sqlGenericService) treeWalkForSqlizer(listCtx *listContext, tslTree tsl

return tslTree, sqlizer, nil
}

func isJSONBSearch(search string) bool {
if strings.Contains(search, "->>") || strings.Contains(search, "@>") {
return true
}
return false
}

func validateJSONBSearch(search string) error {
// handle ->> search. for example: payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels' ->> 'foo' = 'bar'
if strings.Contains(search, "->>") {
jsonbStr := stringSplitwithTrim(search, "->>")
if err := isValidFields(stringSplitwithTrim(jsonbStr[0], "->")); err != nil {
return fmt.Errorf("the search field name is invalid: %v", err)
}
labelStr := stringSplitwithTrim(jsonbStr[1], "=")
if err := validation.IsQualifiedName(labelStr[0]); err != nil {
return fmt.Errorf("the search name is invalid: %v", err)
}
if len(labelStr) > 1 {
if err := validation.IsValidLabelValue(labelStr[1]); err != nil {
return fmt.Errorf("the search value is invalid: %v", err)
}
}
}
// handle @> search. for example: payload -> 'data' -> 'manifests' @> '[{\"metadata\":{\"labels\":{\"foo\":\"bar\"}}}]'
if strings.Contains(search, "@>") {
jsonbStr := stringSplitwithTrim(search, "@>")
if err := isValidFields(stringSplitwithTrim(jsonbStr[0], "->")); err != nil {
return fmt.Errorf("the search field name is invalid: %v", err)
}
// validate the value is a valid json object
if !isValidJSONObject(jsonbStr[1]) {
return fmt.Errorf("the search json is invalid")
}
}
return nil
}

func isValidFields(fields []string) []string {
for _, field := range fields {
if err := validation.IsQualifiedName(field); err != nil {
return err
}
}
return nil
}

func isValidJSONObject(s string) bool {
var js []map[string]interface{}
err := json.Unmarshal([]byte(s), &js)
return err == nil
}

// stringSplitwithTrim trims space and single quotes
func stringSplitwithTrim(s string, sep string) []string {
slices := strings.Split(s, sep)
for i := range slices {
slices[i] = strings.ReplaceAll(slices[i], "'", "")
slices[i] = strings.TrimSpace(slices[i])
}
return slices
}
71 changes: 71 additions & 0 deletions pkg/services/generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,74 @@ func TestSQLTranslation(t *testing.T) {
Expect(values).To(valuesReal)
}
}

func TestValidateJsonbSearch(t *testing.T) {
RegisterTestingT(t)
tests := []map[string]interface{}{
{
"name": "valid ->> search",
"search": "payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels' ->> 'foo' = 'bar'",
},
{
"name": "invalid field name with SQL injection",
"search": "payload -> 'data; drop db;' -> 'manifest' -> 'metadata' -> 'labels' ->> 'foo' = 'bar'",
"error": "the search field name is invalid",
},
{
"name": "invalid name with SQL injection",
"search": "payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels' ->> 'foo;drop db' = 'bar'",
"error": "the search name is invalid",
},
{
"name": "invalid value",
"search": "payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels' ->> 'foo' = '###'",
"error": "the search value is invalid",
},
{
"name": "emtpty value is valid",
"search": "payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels' ->> 'foo' = ",
},
{
"name": "complex labels",
"search": "payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels' ->> 'example.com/version'",
},
{
"name": "valid @> search",
"search": "payload -> 'data' -> 'manifests' @> '[{\"metadata\":{\"labels\":{\"foo\":\"bar\"}}}]'",
},
{
"name": "invalid json object, must be an array",
"search": "payload -> 'data' -> 'manifests' @> '{\"metadata\":{\"labels\":{\"foo\":\"bar\"}}}'",
"error": "the search json is invalid",
},
{
"name": "invalid json object, missed }",
"search": "payload -> 'data' -> 'manifests' @> '[{\"metadata\":{\"labels\":{\"foo\":\"bar\"}}]'",
"error": "the search json is invalid",
},
{
"name": "invalid json object with SQL injection",
"search": "payload -> 'data' -> 'manifests' @> '[{\"metadata\":{\"labels\":{\"foo\":\";drop table xx;\"}}}]'",
"error": "the search json is invalid",
},
{
"name": "invalid search without field name",
"search": "->>",
"error": "the search field name is invalid",
},
{
"name": "invalid search with two ->> symbols",
"search": "payload -> 'data' -> 'manifest' -> 'metadata' -> 'labels'->> 'foo' = 'bar' AND 'labels'->> 'foo'",
"error": "the search value is invalid",
},
}
for _, test := range tests {
t.Run(test["name"].(string), func(t *testing.T) {
search := test["search"].(string)
err := validateJSONBSearch(search)
if err != nil {
Expect(err.Error()).To(ContainSubstring(test["error"].(string)))
}
})
}
}

0 comments on commit 354826f

Please sign in to comment.