Skip to content

Commit f92516c

Browse files
authored
Access Control Options 🔒 (#29)
Adding access control options, making the converter error if a disallowed column/field is queried. Previously this library has been insecure by default, users could easily make the mistake of opening up their entire database. This change makes it required to supply at least one access control option. New options: - `filter.WithAllowAllColumns()` Allow filtering of all columns, same as the previous behaviour - `filter.WithAllowColumns(...)` Allow only selected columns - `filter.WithDisallowColumns(...)` Disallow certain columns, used in combination with WithAllowAllColumns() and WithNestedJSONB(). Note: This change is not backwards compatible! ⚠️
1 parent 9061a61 commit f92516c

9 files changed

+264
-57
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ import (
3232

3333
func main() {
3434
// Create a converter with options:
35+
// - WithAllowAllColumns: allow all columns to be filtered.
3536
// - WithArrayDriver: to convert arrays to the correct driver type, required when using lib/pq
36-
converter := filter.NewConverter(filter.WithArrayDriver(pq.Array))
37+
converter, err := filter.NewConverter(filter.WithAllowAllColumns(), filter.WithArrayDriver(pq.Array))
38+
if err != nil {
39+
// handle error
40+
}
3741

3842
// Convert a filter query to a WHERE clause and values:
3943
input := []byte(`{"title": "Jurassic Park"}`)

examples/basic_test.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99

1010
func ExampleNewConverter() {
1111
// Remeber to use `filter.WithArrayDriver(pg.Array)` when using github.com/lib/pq
12-
converter := filter.NewConverter(filter.WithNestedJSONB("meta", "created_at", "updated_at"))
12+
converter, err := filter.NewConverter(filter.WithNestedJSONB("meta", "created_at", "updated_at"))
13+
if err != nil {
14+
// handle error
15+
}
1316

1417
mongoFilterQuery := `{
1518
"name": "John",
@@ -30,7 +33,10 @@ func ExampleNewConverter() {
3033
}
3134

3235
func ExampleNewConverter_emptyfilter() {
33-
converter := filter.NewConverter(filter.WithEmptyCondition("TRUE")) // The default is FALSE if you don't change it.
36+
converter, err := filter.NewConverter(filter.WithAllowAllColumns(), filter.WithEmptyCondition("TRUE")) // The default is FALSE if you don't change it.
37+
if err != nil {
38+
// handle error
39+
}
3440

3541
mongoFilterQuery := `{}`
3642
conditions, _, err := converter.Convert([]byte(mongoFilterQuery), 1)
@@ -44,7 +50,10 @@ func ExampleNewConverter_emptyfilter() {
4450
}
4551

4652
func ExampleNewConverter_nonIsolatedConditions() {
47-
converter := filter.NewConverter()
53+
converter, err := filter.NewConverter(filter.WithAllowAllColumns())
54+
if err != nil {
55+
// handle error
56+
}
4857

4958
mongoFilterQuery := `{
5059
"$or": [

examples/readme_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88

99
func ExampleNewConverter_readme() {
1010
// Remeber to use `filter.WithArrayDriver(pg.Array)` when using github.com/lib/pq
11-
converter := filter.NewConverter(filter.WithNestedJSONB("meta", "created_at", "updated_at"))
11+
converter, err := filter.NewConverter(filter.WithNestedJSONB("meta", "created_at", "updated_at"))
12+
if err != nil {
13+
// handle error
14+
}
1215

1316
mongoFilterQuery := `{
1417
"$and": [

filter/converter.go

+40-7
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ const defaultPlaceholderName = "__filter_placeholder"
3030

3131
// Converter converts MongoDB filter queries to SQL conditions and values. Use [filter.NewConverter] to create a new instance.
3232
type Converter struct {
33-
nestedColumn string
34-
nestedExemptions []string
35-
arrayDriver func(a any) interface {
33+
allowAllColumns bool
34+
allowedColumns []string
35+
disallowedColumns []string
36+
nestedColumn string
37+
nestedExemptions []string
38+
arrayDriver func(a any) interface {
3639
driver.Valuer
3740
sql.Scanner
3841
}
@@ -45,16 +48,23 @@ type Converter struct {
4548
// NewConverter creates a new [Converter] with optional nested JSONB field mapping.
4649
//
4750
// Note: When using https://github.com/lib/pq, the [filter.WithArrayDriver] should be set to pq.Array.
48-
func NewConverter(options ...Option) *Converter {
51+
func NewConverter(options ...Option) (*Converter, error) {
4952
converter := &Converter{
5053
// don't set defaults, use the once.Do in #Convert()
5154
}
55+
seenAccessOption := false
5256
for _, option := range options {
53-
if option != nil {
54-
option(converter)
57+
if option.f != nil {
58+
option.f(converter)
5559
}
60+
if option.isAccessOption {
61+
seenAccessOption = true
62+
}
63+
}
64+
if !seenAccessOption {
65+
return nil, ErrNoAccessOption
5666
}
57-
return converter
67+
return converter, nil
5868
}
5969

6070
// Convert converts a MongoDB filter query into SQL conditions and values.
@@ -165,6 +175,9 @@ func (c *Converter) convertFilter(filter map[string]any, paramIndex int) (string
165175
if !isValidPostgresIdentifier(key) {
166176
return "", nil, fmt.Errorf("invalid column name: %s", key)
167177
}
178+
if !c.isColumnAllowed(key) {
179+
return "", nil, ColumnNotAllowedError{Column: key}
180+
}
168181

169182
switch v := value.(type) {
170183
case map[string]any:
@@ -346,6 +359,26 @@ func (c *Converter) columnName(column string) string {
346359
return fmt.Sprintf(`%q->>'%s'`, c.nestedColumn, column)
347360
}
348361

362+
func (c *Converter) isColumnAllowed(column string) bool {
363+
for _, disallowed := range c.disallowedColumns {
364+
if disallowed == column {
365+
return false
366+
}
367+
}
368+
if c.allowAllColumns {
369+
return true
370+
}
371+
if c.nestedColumn != "" {
372+
return true
373+
}
374+
for _, allowed := range c.allowedColumns {
375+
if allowed == column {
376+
return true
377+
}
378+
}
379+
return false
380+
}
381+
349382
func (c *Converter) isNestedColumn(column string) bool {
350383
if c.nestedColumn == "" {
351384
return false

0 commit comments

Comments
 (0)