Add ability to block queries and mutations by role
This commit is contained in:
parent
ff13f651d6
commit
6d2f334011
|
@ -145,13 +145,13 @@ roles:
|
||||||
aggregation: false
|
aggregation: false
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
update:
|
update:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
- name: user
|
- name: user
|
||||||
tables:
|
tables:
|
||||||
|
@ -181,7 +181,7 @@ roles:
|
||||||
- updated_at: "now"
|
- updated_at: "now"
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
deny: true
|
block: true
|
||||||
|
|
||||||
- name: admin
|
- name: admin
|
||||||
match: id = 1
|
match: id = 1
|
||||||
|
|
|
@ -133,13 +133,13 @@ roles:
|
||||||
aggregation: false
|
aggregation: false
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
update:
|
update:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
- name: user
|
- name: user
|
||||||
tables:
|
tables:
|
||||||
|
@ -169,7 +169,7 @@ roles:
|
||||||
- updated_at: "now"
|
- updated_at: "now"
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
deny: true
|
block: true
|
||||||
|
|
||||||
- name: admin
|
- name: admin
|
||||||
match: id = 1
|
match: id = 1
|
||||||
|
|
|
@ -99,6 +99,10 @@ func (c *compilerContext) renderInsert(qc *qcode.QCode, w io.Writer,
|
||||||
io.WriteString(c.w, ti.Name)
|
io.WriteString(c.w, ti.Name)
|
||||||
io.WriteString(c.w, `, i.j) t`)
|
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
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
func TestCompileMutate(t *testing.T) {
|
||||||
t.Run("simpleInsert", simpleInsert)
|
t.Run("simpleInsert", simpleInsert)
|
||||||
t.Run("singleInsert", singleInsert)
|
t.Run("singleInsert", singleInsert)
|
||||||
|
@ -179,6 +226,7 @@ func TestCompileMutate(t *testing.T) {
|
||||||
t.Run("singleUpdate", singleUpdate)
|
t.Run("singleUpdate", singleUpdate)
|
||||||
t.Run("singleUpsert", singleUpsert)
|
t.Run("singleUpsert", singleUpsert)
|
||||||
t.Run("bulkUpsert", bulkUpsert)
|
t.Run("bulkUpsert", bulkUpsert)
|
||||||
|
|
||||||
t.Run("delete", delete)
|
t.Run("delete", delete)
|
||||||
|
t.Run("blockedInsert", blockedInsert)
|
||||||
|
t.Run("blockedUpdate", blockedUpdate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -739,12 +739,18 @@ func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error
|
||||||
io.WriteString(c.w, ` OR `)
|
io.WriteString(c.w, ` OR `)
|
||||||
case qcode.OpNot:
|
case qcode.OpNot:
|
||||||
io.WriteString(c.w, `NOT `)
|
io.WriteString(c.w, `NOT `)
|
||||||
|
case qcode.OpFalse:
|
||||||
|
io.WriteString(c.w, `false`)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("11: unexpected value %v (%t)", intf, intf)
|
return fmt.Errorf("11: unexpected value %v (%t)", intf, intf)
|
||||||
}
|
}
|
||||||
|
|
||||||
case *qcode.Exp:
|
case *qcode.Exp:
|
||||||
switch val.Op {
|
switch val.Op {
|
||||||
|
case qcode.OpFalse:
|
||||||
|
st.Push(val.Op)
|
||||||
|
qcode.FreeExp(val)
|
||||||
|
|
||||||
case qcode.OpAnd, qcode.OpOr:
|
case qcode.OpAnd, qcode.OpOr:
|
||||||
for i := len(val.Children) - 1; i >= 0; i-- {
|
for i := len(val.Children) - 1; i >= 0; i-- {
|
||||||
st.Push(val.Children[i])
|
st.Push(val.Children[i])
|
||||||
|
|
|
@ -2,182 +2,9 @@ package psql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"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) {
|
func withComplexArgs(t *testing.T) {
|
||||||
gql := `query {
|
gql := `query {
|
||||||
proDUcts(
|
proDUcts(
|
||||||
|
@ -561,6 +388,7 @@ func TestCompileQuery(t *testing.T) {
|
||||||
t.Run("aggFunctionWithFilter", aggFunctionWithFilter)
|
t.Run("aggFunctionWithFilter", aggFunctionWithFilter)
|
||||||
t.Run("syntheticTables", syntheticTables)
|
t.Run("syntheticTables", syntheticTables)
|
||||||
t.Run("queryWithVariables", queryWithVariables)
|
t.Run("queryWithVariables", queryWithVariables)
|
||||||
|
t.Run("blockedQuery", blockedQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
var benchGQL = []byte(`query {
|
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) {
|
func BenchmarkCompile(b *testing.B) {
|
||||||
w := &bytes.Buffer{}
|
w := &bytes.Buffer{}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ const (
|
||||||
OpIsNull
|
OpIsNull
|
||||||
OpEqID
|
OpEqID
|
||||||
OpTsQuery
|
OpTsQuery
|
||||||
|
OpFalse
|
||||||
)
|
)
|
||||||
|
|
||||||
type ValType int
|
type ValType int
|
||||||
|
@ -364,18 +365,24 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error {
|
||||||
fil = trv.filter(qc.Type)
|
fil = trv.filter(qc.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fil != nil && fil.Op != OpNop {
|
if fil != nil {
|
||||||
if root.Where != nil {
|
switch fil.Op {
|
||||||
ow := root.Where
|
case OpNop:
|
||||||
|
case OpFalse:
|
||||||
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
|
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 {
|
for i := range filter {
|
||||||
|
if filter[i] == "false" {
|
||||||
|
return &Exp{Op: OpFalse, doFree: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
node, err := ParseArgValue(filter[i])
|
node, err := ParseArgValue(filter[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -109,27 +109,27 @@ type configRole struct {
|
||||||
Filters []string
|
Filters []string
|
||||||
Columns []string
|
Columns []string
|
||||||
DisableAggregation bool `mapstructure:"disable_aggregation"`
|
DisableAggregation bool `mapstructure:"disable_aggregation"`
|
||||||
Deny bool
|
Block bool
|
||||||
}
|
}
|
||||||
|
|
||||||
Insert struct {
|
Insert struct {
|
||||||
Filters []string
|
Filters []string
|
||||||
Columns []string
|
Columns []string
|
||||||
Set map[string]string
|
Set map[string]string
|
||||||
Deny bool
|
Block bool
|
||||||
}
|
}
|
||||||
|
|
||||||
Update struct {
|
Update struct {
|
||||||
Filters []string
|
Filters []string
|
||||||
Columns []string
|
Columns []string
|
||||||
Set map[string]string
|
Set map[string]string
|
||||||
Deny bool
|
Block bool
|
||||||
}
|
}
|
||||||
|
|
||||||
Delete struct {
|
Delete struct {
|
||||||
Filters []string
|
Filters []string
|
||||||
Columns []string
|
Columns []string
|
||||||
Deny bool
|
Block bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
serv/serv.go
18
serv/serv.go
|
@ -30,6 +30,8 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockFilter := []string{"false"}
|
||||||
|
|
||||||
for _, r := range c.Roles {
|
for _, r := range c.Roles {
|
||||||
for _, t := range r.Tables {
|
for _, t := range r.Tables {
|
||||||
query := qcode.QueryConfig{
|
query := qcode.QueryConfig{
|
||||||
|
@ -39,23 +41,39 @@ func initCompilers(c *config) (*qcode.Compiler, *psql.Compiler, error) {
|
||||||
DisableFunctions: t.Query.DisableAggregation,
|
DisableFunctions: t.Query.DisableAggregation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Query.Block {
|
||||||
|
query.Filters = blockFilter
|
||||||
|
}
|
||||||
|
|
||||||
insert := qcode.InsertConfig{
|
insert := qcode.InsertConfig{
|
||||||
Filters: t.Insert.Filters,
|
Filters: t.Insert.Filters,
|
||||||
Columns: t.Insert.Columns,
|
Columns: t.Insert.Columns,
|
||||||
Set: t.Insert.Set,
|
Set: t.Insert.Set,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Query.Block {
|
||||||
|
insert.Filters = blockFilter
|
||||||
|
}
|
||||||
|
|
||||||
update := qcode.UpdateConfig{
|
update := qcode.UpdateConfig{
|
||||||
Filters: t.Insert.Filters,
|
Filters: t.Insert.Filters,
|
||||||
Columns: t.Insert.Columns,
|
Columns: t.Insert.Columns,
|
||||||
Set: t.Insert.Set,
|
Set: t.Insert.Set,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Query.Block {
|
||||||
|
update.Filters = blockFilter
|
||||||
|
}
|
||||||
|
|
||||||
delete := qcode.DeleteConfig{
|
delete := qcode.DeleteConfig{
|
||||||
Filters: t.Insert.Filters,
|
Filters: t.Insert.Filters,
|
||||||
Columns: t.Insert.Columns,
|
Columns: t.Insert.Columns,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Query.Block {
|
||||||
|
delete.Filters = blockFilter
|
||||||
|
}
|
||||||
|
|
||||||
qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
qc.AddRole(r.Name, t.Name, qcode.TRConfig{
|
||||||
Query: query,
|
Query: query,
|
||||||
Insert: insert,
|
Insert: insert,
|
||||||
|
|
|
@ -145,13 +145,13 @@ roles:
|
||||||
aggregation: false
|
aggregation: false
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
update:
|
update:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
- name: user
|
- name: user
|
||||||
tables:
|
tables:
|
||||||
|
|
|
@ -133,13 +133,13 @@ roles:
|
||||||
aggregation: false
|
aggregation: false
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
update:
|
update:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
allow: false
|
block: false
|
||||||
|
|
||||||
- name: user
|
- name: user
|
||||||
tables:
|
tables:
|
||||||
|
|
Loading…
Reference in New Issue