feat(03-02): add allow-listed database-default order helper
- Reject identifiers and directions outside the caller allow-list - Emit ordinary ORDER BY without COLLATE so ICU pl-PL applies Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
46
lagoon/order.go
Normal file
46
lagoon/order.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// OrderBy appends a database-default ORDER BY for an allow-listed qualified
|
||||
// column. Identifiers and directions are never taken from untrusted input:
|
||||
// column must match allowed exactly, and dir must be asc or desc. No COLLATE
|
||||
// is emitted; Postgres ICU pl-PL is the database default (CheckLocale).
|
||||
func OrderBy(db *gorm.DB, column, dir string, allowed []string) (*gorm.DB, error) {
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("lagoon: gorm db is nil")
|
||||
}
|
||||
clause, err := orderClause(column, dir, allowed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.Order(clause), nil
|
||||
}
|
||||
|
||||
func orderClause(column, dir string, allowed []string) (string, error) {
|
||||
if !allowListed(column, allowed) {
|
||||
return "", fmt.Errorf("lagoon: order column %q is not allow-listed", column)
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(dir)) {
|
||||
case "asc":
|
||||
return column + " ASC", nil
|
||||
case "desc":
|
||||
return column + " DESC", nil
|
||||
default:
|
||||
return "", fmt.Errorf("lagoon: order direction %q is not allow-listed", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func allowListed(column string, allowed []string) bool {
|
||||
for _, a := range allowed {
|
||||
if a == column {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
39
lagoon/order_test.go
Normal file
39
lagoon/order_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrderClauseAllowList(t *testing.T) {
|
||||
allowed := []string{"golem15_fonoteka_genres.name", "items.title"}
|
||||
|
||||
got, err := orderClause("golem15_fonoteka_genres.name", "asc", allowed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "golem15_fonoteka_genres.name ASC" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(got), "collate") {
|
||||
t.Fatalf("must not emit COLLATE: %q", got)
|
||||
}
|
||||
|
||||
got, err = orderClause("items.title", "DESC", allowed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "items.title DESC" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
|
||||
if _, err := orderClause("golem15_fonoteka_genres.name;drop table x", "asc", allowed); err == nil {
|
||||
t.Fatal("want reject unknown column")
|
||||
}
|
||||
if _, err := orderClause("golem15_fonoteka_genres.name", "ascending", allowed); err == nil {
|
||||
t.Fatal("want reject unknown direction")
|
||||
}
|
||||
if _, err := OrderBy(nil, "items.title", "asc", allowed); err == nil {
|
||||
t.Fatal("want nil db error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user