diff --git a/config/dev.yml b/config/dev.yml index ffe9f64..ee9b756 100644 --- a/config/dev.yml +++ b/config/dev.yml @@ -145,13 +145,13 @@ roles: aggregation: false insert: - allow: false + block: false update: - allow: false + block: false delete: - allow: false + block: false - name: user tables: @@ -181,7 +181,7 @@ roles: - updated_at: "now" delete: - deny: true + block: true - name: admin match: id = 1 diff --git a/config/prod.yml b/config/prod.yml index 95abfb7..e070427 100644 --- a/config/prod.yml +++ b/config/prod.yml @@ -133,13 +133,13 @@ roles: aggregation: false insert: - allow: false + block: false update: - allow: false + block: false delete: - allow: false + block: false - name: user tables: @@ -169,7 +169,7 @@ roles: - updated_at: "now" delete: - deny: true + block: true - name: admin match: id = 1 diff --git a/psql/mutate.go b/psql/mutate.go index 84bb122..bf15004 100644 --- a/psql/mutate.go +++ b/psql/mutate.go @@ -99,6 +99,10 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer, io.WriteString(c.w, ti.Name) io.WriteString(c.w, `, i.j) t`) + if w := qc.Selects[0].Where; w != nil && w.Op == qcode.OpFalse { + io.WriteString(c.w, ` WHERE false`) + } + return 0, nil } diff --git a/psql/mutate_test.go b/psql/mutate_test.go index 390c301..29bf153 100644 --- a/psql/mutate_test.go +++ b/psql/mutate_test.go @@ -172,6 +172,53 @@ func delete(t *testing.T) { } } +func blockedInsert(t *testing.T) { + gql := `mutation { + user(insert: $data) { + id + } + }` + + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) INSERT INTO "users" (full_name, email) SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id" FROM "users") AS "users_0") AS "done_1337"` + + vars := map[string]json.RawMessage{ + "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), + } + + resSQL, err := compileGQLToPSQL(gql, vars, "bad_dude") + if err != nil { + t.Fatal(err) + } + + if string(resSQL) != sql { + t.Fatal(errNotExpected) + } +} + +func blockedUpdate(t *testing.T) { + gql := `mutation { + user(where: { id: { lt: 5 } }, update: $data) { + id + email + } + }` + + sql := `WITH "users" AS (WITH "input" AS (SELECT {{data}}::json AS j) UPDATE "users" SET (full_name, email) = (SELECT full_name, email FROM input i, json_populate_record(NULL::users, i.j) t) WHERE false RETURNING *) SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."email" FROM "users") AS "users_0") AS "done_1337"` + + vars := map[string]json.RawMessage{ + "data": json.RawMessage(`{"email": "reannagreenholt@orn.com", "full_name": "Flo Barton"}`), + } + + resSQL, err := compileGQLToPSQL(gql, vars, "bad_dude") + if err != nil { + t.Fatal(err) + } + + if string(resSQL) != sql { + t.Fatal(errNotExpected) + } +} + func TestCompileMutate(t *testing.T) { t.Run("simpleInsert", simpleInsert) t.Run("singleInsert", singleInsert) @@ -179,6 +226,7 @@ func TestCompileMutate(t *testing.T) { t.Run("singleUpdate", singleUpdate) t.Run("singleUpsert", singleUpsert) t.Run("bulkUpsert", bulkUpsert) - t.Run("delete", delete) + t.Run("blockedInsert", blockedInsert) + t.Run("blockedUpdate", blockedUpdate) } diff --git a/psql/psql_test.go b/psql/psql_test.go new file mode 100644 index 0000000..f707ae1 --- /dev/null +++ b/psql/psql_test.go @@ -0,0 +1,190 @@ +package psql + +import ( + "log" + "os" + "testing" + + "github.com/dosco/super-graph/qcode" +) + +const ( + errNotExpected = "Generated SQL did not match what was expected" +) + +var ( + qcompile *qcode.Compiler + pcompile *Compiler +) + +func TestMain(m *testing.M) { + var err error + + qcompile, err = qcode.NewCompiler(qcode.Config{ + Blocklist: []string{ + "secret", + "password", + "token", + }, + }) + + qcompile.AddRole("user", "product", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Columns: []string{"id", "name", "price", "users", "customers"}, + Filters: []string{ + "{ price: { gt: 0 } }", + "{ price: { lt: 8 } }", + }, + }, + Update: qcode.UpdateConfig{ + Filters: []string{"{ user_id: { eq: $user_id } }"}, + }, + Delete: qcode.DeleteConfig{ + Filters: []string{ + "{ price: { gt: 0 } }", + "{ price: { lt: 8 } }", + }, + }, + }) + + qcompile.AddRole("anon", "product", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Columns: []string{"id", "name"}, + }, + }) + + qcompile.AddRole("anon1", "product", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Columns: []string{"id", "name", "price"}, + DisableFunctions: true, + }, + }) + + qcompile.AddRole("user", "users", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Columns: []string{"id", "full_name", "avatar", "email", "products"}, + }, + }) + + qcompile.AddRole("bad_dude", "users", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Filters: []string{"false"}, + }, + Insert: qcode.InsertConfig{ + Filters: []string{"false"}, + }, + Update: qcode.UpdateConfig{ + Filters: []string{"false"}, + }, + }) + + qcompile.AddRole("user", "mes", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Columns: []string{"id", "full_name", "avatar"}, + Filters: []string{ + "{ id: { eq: $user_id } }", + }, + }, + }) + + qcompile.AddRole("user", "customers", qcode.TRConfig{ + Query: qcode.QueryConfig{ + Columns: []string{"id", "email", "full_name", "products"}, + }, + }) + + if err != nil { + log.Fatal(err) + } + + tables := []*DBTable{ + &DBTable{Name: "customers", Type: "table"}, + &DBTable{Name: "users", Type: "table"}, + &DBTable{Name: "products", Type: "table"}, + &DBTable{Name: "purchases", Type: "table"}, + } + + columns := [][]*DBColumn{ + []*DBColumn{ + &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 5, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 6, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 7, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 8, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, + []*DBColumn{ + &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 5, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 6, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 7, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 8, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 9, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, + []*DBColumn{ + &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 5, Name: "user_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "users", FKeyColID: []int16{1}}, + &DBColumn{ID: 6, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 7, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, + []*DBColumn{ + &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}}, + &DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}}, + &DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 5, Name: "quantity", Type: "integer", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, + &DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, + } + + schema := &DBSchema{ + t: make(map[string]*DBTableInfo), + rm: make(map[string]map[string]*DBRel), + al: make(map[string]struct{}), + } + + aliases := map[string][]string{ + "users": []string{"mes"}, + } + + for i, t := range tables { + schema.updateSchema(t, columns[i], aliases) + } + + vars := NewVariables(map[string]string{ + "account_id": "select account_id from users where id = $user_id", + }) + + pcompile = NewCompiler(Config{ + Schema: schema, + Vars: vars, + }) + + os.Exit(m.Run()) +} + +func compileGQLToPSQL(gql string, vars Variables, role string) ([]byte, error) { + qc, err := qcompile.Compile([]byte(gql), role) + if err != nil { + return nil, err + } + + _, sqlStmt, err := pcompile.CompileEx(qc, vars) + if err != nil { + return nil, err + } + + //fmt.Println(string(sqlStmt)) + + return sqlStmt, nil +} diff --git a/psql/query.go b/psql/query.go index c06ab95..7fc2cd3 100644 --- a/psql/query.go +++ b/psql/query.go @@ -739,12 +739,18 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error io.WriteString(c.w, ` OR `) case qcode.OpNot: io.WriteString(c.w, `NOT `) + case qcode.OpFalse: + io.WriteString(c.w, `false`) default: return fmt.Errorf("11: unexpected value %v (%t)", intf, intf) } case *qcode.Exp: switch val.Op { + case qcode.OpFalse: + st.Push(val.Op) + qcode.FreeExp(val) + case qcode.OpAnd, qcode.OpOr: for i := len(val.Children) - 1; i >= 0; i-- { st.Push(val.Children[i]) diff --git a/psql/query_test.go b/psql/query_test.go index 78330b6..b00c8e6 100644 --- a/psql/query_test.go +++ b/psql/query_test.go @@ -2,182 +2,9 @@ package psql import ( "bytes" - "log" - "os" "testing" - - "github.com/dosco/super-graph/qcode" ) -const ( - errNotExpected = "Generated SQL did not match what was expected" -) - -var ( - qcompile *qcode.Compiler - pcompile *Compiler -) - -func TestMain(m *testing.M) { - var err error - - qcompile, err = qcode.NewCompiler(qcode.Config{ - Blocklist: []string{ - "secret", - "password", - "token", - }, - }) - - qcompile.AddRole("user", "product", qcode.TRConfig{ - Query: qcode.QueryConfig{ - Columns: []string{"id", "name", "price", "users", "customers"}, - Filters: []string{ - "{ price: { gt: 0 } }", - "{ price: { lt: 8 } }", - }, - }, - Update: qcode.UpdateConfig{ - Filters: []string{"{ user_id: { eq: $user_id } }"}, - }, - Delete: qcode.DeleteConfig{ - Filters: []string{ - "{ price: { gt: 0 } }", - "{ price: { lt: 8 } }", - }, - }, - }) - - qcompile.AddRole("anon", "product", qcode.TRConfig{ - Query: qcode.QueryConfig{ - Columns: []string{"id", "name"}, - }, - }) - - qcompile.AddRole("anon1", "product", qcode.TRConfig{ - Query: qcode.QueryConfig{ - Columns: []string{"id", "name", "price"}, - DisableFunctions: true, - }, - }) - - qcompile.AddRole("user", "users", qcode.TRConfig{ - Query: qcode.QueryConfig{ - Columns: []string{"id", "full_name", "avatar", "email", "products"}, - }, - }) - - qcompile.AddRole("user", "mes", qcode.TRConfig{ - Query: qcode.QueryConfig{ - Columns: []string{"id", "full_name", "avatar"}, - Filters: []string{ - "{ id: { eq: $user_id } }", - }, - }, - }) - - qcompile.AddRole("user", "customers", qcode.TRConfig{ - Query: qcode.QueryConfig{ - Columns: []string{"id", "email", "full_name", "products"}, - }, - }) - - if err != nil { - log.Fatal(err) - } - - tables := []*DBTable{ - &DBTable{Name: "customers", Type: "table"}, - &DBTable{Name: "users", Type: "table"}, - &DBTable{Name: "products", Type: "table"}, - &DBTable{Name: "purchases", Type: "table"}, - } - - columns := [][]*DBColumn{ - []*DBColumn{ - &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 4, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 5, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 6, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 7, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 8, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 9, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 10, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, - []*DBColumn{ - &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 2, Name: "full_name", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 3, Name: "phone", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 4, Name: "avatar", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 5, Name: "email", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 6, Name: "encrypted_password", Type: "character varying", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 7, Name: "reset_password_token", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 8, Name: "reset_password_sent_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 9, Name: "remember_created_at", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 10, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 11, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, - []*DBColumn{ - &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 2, Name: "name", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 3, Name: "description", Type: "text", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 4, Name: "price", Type: "numeric(7,2)", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 5, Name: "user_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "users", FKeyColID: []int16{1}}, - &DBColumn{ID: 6, Name: "created_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 7, Name: "updated_at", Type: "timestamp without time zone", NotNull: true, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 8, Name: "tsv", Type: "tsvector", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, - []*DBColumn{ - &DBColumn{ID: 1, Name: "id", Type: "bigint", NotNull: true, PrimaryKey: true, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 2, Name: "customer_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "customers", FKeyColID: []int16{1}}, - &DBColumn{ID: 3, Name: "product_id", Type: "bigint", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "products", FKeyColID: []int16{1}}, - &DBColumn{ID: 4, Name: "sale_type", Type: "character varying", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 5, Name: "quantity", Type: "integer", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 6, Name: "due_date", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}, - &DBColumn{ID: 7, Name: "returned", Type: "timestamp without time zone", NotNull: false, PrimaryKey: false, UniqueKey: false, FKeyTable: "", FKeyColID: []int16(nil)}}, - } - - schema := &DBSchema{ - t: make(map[string]*DBTableInfo), - rm: make(map[string]map[string]*DBRel), - al: make(map[string]struct{}), - } - - aliases := map[string][]string{ - "users": []string{"mes"}, - } - - for i, t := range tables { - schema.updateSchema(t, columns[i], aliases) - } - - vars := NewVariables(map[string]string{ - "account_id": "select account_id from users where id = $user_id", - }) - - pcompile = NewCompiler(Config{ - Schema: schema, - Vars: vars, - }) - - os.Exit(m.Run()) -} - -func compileGQLToPSQL(gql string, vars Variables, role string) ([]byte, error) { - qc, err := qcompile.Compile([]byte(gql), role) - if err != nil { - return nil, err - } - - _, sqlStmt, err := pcompile.CompileEx(qc, vars) - if err != nil { - return nil, err - } - - //fmt.Println(string(sqlStmt)) - - return sqlStmt, nil -} - func withComplexArgs(t *testing.T) { gql := `query { proDUcts( @@ -561,6 +388,7 @@ func TestCompileQuery(t *testing.T) { t.Run("aggFunctionWithFilter", aggFunctionWithFilter) t.Run("syntheticTables", syntheticTables) t.Run("queryWithVariables", queryWithVariables) + t.Run("blockedQuery", blockedQuery) } var benchGQL = []byte(`query { @@ -586,6 +414,27 @@ var benchGQL = []byte(`query { } }`) +func blockedQuery(t *testing.T) { + gql := `query { + user(id: 5, where: { id: { gt: 3 } }) { + id + full_name + email + } + }` + + sql := `SELECT json_object_agg('user', sel_json_0) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."id" AS "id", "users_0"."full_name" AS "full_name", "users_0"."email" AS "email") AS "sel_0")) AS "sel_json_0" FROM (SELECT "users"."id", "users"."full_name", "users"."email" FROM "users" WHERE (false) LIMIT ('1') :: integer) AS "users_0" LIMIT ('1') :: integer) AS "done_1337"` + + resSQL, err := compileGQLToPSQL(gql, nil, "bad_dude") + if err != nil { + t.Fatal(err) + } + + if string(resSQL) != sql { + t.Fatal(errNotExpected) + } +} + func BenchmarkCompile(b *testing.B) { w := &bytes.Buffer{} diff --git a/qcode/qcode.go b/qcode/qcode.go index 8c90d93..5b8a1c2 100644 --- a/qcode/qcode.go +++ b/qcode/qcode.go @@ -113,6 +113,7 @@ const ( OpIsNull OpEqID OpTsQuery + OpFalse ) type ValType int @@ -364,18 +365,24 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { fil = trv.filter(qc.Type) } - if fil != nil && fil.Op != OpNop { - if root.Where != nil { - ow := root.Where - - root.Where = expPool.Get().(*Exp) - root.Where.Reset() - root.Where.Op = OpAnd - root.Where.Children = root.Where.childrenA[:2] - root.Where.Children[0] = fil - root.Where.Children[1] = ow - } else { + if fil != nil { + switch fil.Op { + case OpNop: + case OpFalse: root.Where = fil + default: + if root.Where != nil { + ow := root.Where + + root.Where = expPool.Get().(*Exp) + root.Where.Reset() + root.Where.Op = OpAnd + root.Where.Children = root.Where.childrenA[:2] + root.Where.Children[0] = fil + root.Where.Children[1] = ow + } else { + root.Where = fil + } } } @@ -950,6 +957,10 @@ func compileFilter(filter []string) (*Exp, error) { } for i := range filter { + if filter[i] == "false" { + return &Exp{Op: OpFalse, doFree: false}, nil + } + node, err := ParseArgValue(filter[i]) if err != nil { return nil, err diff --git a/serv/config.go b/serv/config.go index 160cd4b..ab93f02 100644 --- a/serv/config.go +++ b/serv/config.go @@ -109,27 +109,27 @@ type configRole struct { Filters []string Columns []string DisableAggregation bool `mapstructure:"disable_aggregation"` - Deny bool + Block bool } Insert struct { Filters []string Columns []string Set map[string]string - Deny bool + Block bool } Update struct { Filters []string Columns []string Set map[string]string - Deny bool + Block bool } Delete struct { Filters []string Columns []string - Deny bool + Block bool } } } diff --git a/serv/serv.go b/serv/serv.go index a98c16e..817907f 100644 --- a/serv/serv.go +++ b/serv/serv.go @@ -30,6 +30,8 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) { return nil, nil, err } + blockFilter := []string{"false"} + for _, r := range c.Roles { for _, t := range r.Tables { query := qcode.QueryConfig{ @@ -39,23 +41,39 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) { DisableFunctions: t.Query.DisableAggregation, } + if t.Query.Block { + query.Filters = blockFilter + } + insert := qcode.InsertConfig{ Filters: t.Insert.Filters, Columns: t.Insert.Columns, Set: t.Insert.Set, } + if t.Query.Block { + insert.Filters = blockFilter + } + update := qcode.UpdateConfig{ Filters: t.Insert.Filters, Columns: t.Insert.Columns, Set: t.Insert.Set, } + if t.Query.Block { + update.Filters = blockFilter + } + delete := qcode.DeleteConfig{ Filters: t.Insert.Filters, Columns: t.Insert.Columns, } + if t.Query.Block { + delete.Filters = blockFilter + } + qc.AddRole(r.Name, t.Name, qcode.TRConfig{ Query: query, Insert: insert, diff --git a/tmpl/dev.yml b/tmpl/dev.yml index ffe9f64..1384153 100644 --- a/tmpl/dev.yml +++ b/tmpl/dev.yml @@ -145,13 +145,13 @@ roles: aggregation: false insert: - allow: false + block: false update: - allow: false + block: false delete: - allow: false + block: false - name: user tables: diff --git a/tmpl/prod.yml b/tmpl/prod.yml index 95abfb7..a9a1b9e 100644 --- a/tmpl/prod.yml +++ b/tmpl/prod.yml @@ -133,13 +133,13 @@ roles: aggregation: false insert: - allow: false + block: false update: - allow: false + block: false delete: - allow: false + block: false - name: user tables: