Skip to content

Commit

Permalink
feat(Query): Enable persistent queries in dgraph (#6788)
Browse files Browse the repository at this point in the history
Co-authored-by: Abhimanyu Singh Gaur <[email protected]>
  • Loading branch information
all-seeing-code and abhimanyusinghgaur authored Oct 27, 2020
1 parent d48fed9 commit 3b44d3f
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 38 deletions.
6 changes: 3 additions & 3 deletions dgraph/cmd/alpha/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func TestDeletePredicate(t *testing.T) {
testutil.CompareJSON(t, `{"data":{"schema":[`+
`{"predicate":"age","type":"default"},`+
`{"predicate":"name","type":"string","index":true, "tokenizer":["term"]},`+
x.AclPredicates+","+x.GraphqlPredicates+","+x.CorsPredicate+","+
x.AclPredicates+","+x.GraphqlPredicates+","+x.CorsPredicate+","+x.PersistedQueryPredicate+","+
`{"predicate":"dgraph.type","type":"string","index":true, "tokenizer":["exact"],
"list":true}],`+x.InitialTypes+`}}`, output)

Expand Down Expand Up @@ -1077,7 +1077,7 @@ func TestListTypeSchemaChange(t *testing.T) {
res, err = runGraphqlQuery(q)
require.NoError(t, err)
testutil.CompareJSON(t, `{"data":{"schema":[`+
x.AclPredicates+","+x.GraphqlPredicates+","+x.CorsPredicate+","+
x.AclPredicates+","+x.GraphqlPredicates+","+x.CorsPredicate+","+x.PersistedQueryPredicate+","+
`{"predicate":"occupations","type":"string"},`+
`{"predicate":"dgraph.type", "type":"string", "index":true, "tokenizer": ["exact"],
"list":true}],`+x.InitialTypes+`}}`, res)
Expand Down Expand Up @@ -1325,7 +1325,7 @@ func TestDropAll(t *testing.T) {
require.NoError(t, err)
testutil.CompareJSON(t,
`{"data":{"schema":[`+
x.AclPredicates+","+x.GraphqlPredicates+","+x.CorsPredicate+","+
x.AclPredicates+","+x.GraphqlPredicates+","+x.CorsPredicate+","+x.PersistedQueryPredicate+","+
`{"predicate":"dgraph.type", "type":"string", "index":true, "tokenizer":["exact"],
"list":true}],`+x.InitialTypes+`}}`, output)

Expand Down
2 changes: 2 additions & 0 deletions dgraph/cmd/bulk/systest/test-bulk-schema.sh
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ EOF
diff <(LC_ALL=C sort all_dbs.out | uniq -c) - <<EOF
1 dgraph.acl.rule
1 dgraph.cors
1 dgraph.graphql.p_query
1 dgraph.graphql.p_sha256hash
1 dgraph.graphql.schema
1 dgraph.graphql.schema_created_at
1 dgraph.graphql.schema_history
Expand Down
121 changes: 121 additions & 0 deletions edgraph/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ package edgraph
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"time"

"github.com/dgraph-io/dgo/v200/protos/api"
"github.com/dgraph-io/dgraph/graphql/schema"
"github.com/dgraph-io/ristretto/z"
"github.com/golang/glog"
"github.com/pkg/errors"
)

// ResetCors make the dgraph to accept all the origins if no origins were given
Expand Down Expand Up @@ -151,3 +155,120 @@ func UpdateSchemaHistory(ctx context.Context, schema string) error {
_, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize)
return err
}

// ProcessPersistedQuery stores and retrieves persisted queries by following waterfall logic:
// 1. If sha256Hash is not provided process queries without persisting
// 2. If sha256Hash is provided try retrieving persisted queries
// 2a. Persisted Query not found
// i) If query is not provided then throw "PersistedQueryNotFound"
// ii) If query is provided then store query in dgraph only if sha256 of the query is correct
// otherwise throw "provided sha does not match query"
// 2b. Persisted Query found
// i) If query is not provided then update gqlRes with the found query and proceed
// ii) If query is provided then match query retrieved, if identical do nothing else
// throw "query does not match persisted query"
func ProcessPersistedQuery(ctx context.Context, gqlReq *schema.Request) error {
query := gqlReq.Query
sha256Hash := gqlReq.Extensions.PersistedQuery.Sha256Hash

if sha256Hash == "" {
return nil
}

queryForSHA := `query Me($sha: string){
me(func: eq(dgraph.graphql.p_sha256hash, $sha)){
dgraph.graphql.p_query
}
}`
variables := map[string]string{
"$sha": sha256Hash,
}
req := &api.Request{
Query: queryForSHA,
Vars: variables,
ReadOnly: true,
}

storedQuery, err := (&Server{}).doQuery(ctx, req, NoAuthorize)

if err != nil {
glog.Errorf("Error while querying sha %s", sha256Hash)
return err
}

type shaQueryResponse struct {
Me []struct {
PersistedQuery string `json:"dgraph.graphql.p_query"`
} `json:"me"`
}

shaQueryRes := &shaQueryResponse{}
if len(storedQuery.Json) > 0 {
if err := json.Unmarshal(storedQuery.Json, shaQueryRes); err != nil {
return err
}
}

if len(shaQueryRes.Me) == 0 {
if query == "" {
return errors.New("PersistedQueryNotFound")
}
if match, err := hashMatches(query, sha256Hash); err != nil {
return err
} else if !match {
return errors.New("provided sha does not match query")
}

req := &api.Request{
Mutations: []*api.Mutation{
{
Set: []*api.NQuad{
{
Subject: "_:a",
Predicate: "dgraph.graphql.p_query",
ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: query}},
},
{
Subject: "_:a",
Predicate: "dgraph.graphql.p_sha256hash",
ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: sha256Hash}},
},
{
Subject: "_:a",
Predicate: "dgraph.type",
ObjectValue: &api.Value{Val: &api.Value_StrVal{
StrVal: "dgraph.graphql.persisted_query"}},
},
},
},
},
CommitNow: true,
}

_, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize)
return err

}

if len(shaQueryRes.Me) != 1 {
return fmt.Errorf("same sha returned %d queries", len(shaQueryRes.Me))
}

if len(query) > 0 && shaQueryRes.Me[0].PersistedQuery != query {
return errors.New("query does not match persisted query")
}

gqlReq.Query = shaQueryRes.Me[0].PersistedQuery
return nil

}

func hashMatches(query, sha256Hash string) (bool, error) {
hasher := sha256.New()
_, err := hasher.Write([]byte(query))
if err != nil {
return false, err
}
hashGenerated := hex.EncodeToString(hasher.Sum(nil))
return hashGenerated == sha256Hash, nil
}
42 changes: 33 additions & 9 deletions ee/acl/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func getGrootAndGuardiansUid(t *testing.T, dg *dgo.Dgraph) (string, string) {
GrootUser []userNode `json:"grootUser"`
}

resp,err := txn.Query(ctx, grootUserQuery)
resp, err := txn.Query(ctx, grootUserQuery)
require.NoError(t, err, "groot user query failed")

var userResp userQryResp
Expand Down Expand Up @@ -939,7 +939,7 @@ func TestUnauthorizedDeletion(t *testing.T) {
err = userClient.Login(ctx, userid, userpassword)
require.NoError(t, err)

_, err = deleteUsingNQuad(userClient, "<" + nodeUID + ">", "<" + unAuthPred + ">", "*")
_, err = deleteUsingNQuad(userClient, "<"+nodeUID+">", "<"+unAuthPred+">", "*")

require.Error(t, err)
require.Contains(t, err.Error(), "PermissionDenied")
Expand Down Expand Up @@ -1395,7 +1395,7 @@ func TestDeleteQueryWithACLPermissions(t *testing.T) {
require.NoError(t, err)

// delete S * * (user now has permission to name and age)
_, err = deleteUsingNQuad(userClient, "<" + nodeUID + ">", "*", "*")
_, err = deleteUsingNQuad(userClient, "<"+nodeUID+">", "*", "*")
require.NoError(t, err)

accessJwt, _, err = testutil.HttpLogin(&testutil.LoginParams{
Expand All @@ -1416,7 +1416,7 @@ func TestDeleteQueryWithACLPermissions(t *testing.T) {
time.Sleep(5 * time.Second)

// delete S * * (user now has permission to name, age and dgraph.type)
_, err = deleteUsingNQuad(userClient, "<" + nodeUID + ">", "*", "*")
_, err = deleteUsingNQuad(userClient, "<"+nodeUID+">", "*", "*")
require.NoError(t, err)

accessJwt, _, err = testutil.HttpLogin(&testutil.LoginParams{
Expand Down Expand Up @@ -2146,6 +2146,16 @@ func TestSchemaQueryWithACL(t *testing.T) {
],
"upsert": true
},
{
"predicate":"dgraph.graphql.p_query",
"type":"string"
},
{
"predicate":"dgraph.graphql.p_sha256hash",
"type":"string",
"index":true,
"tokenizer":["exact"]
},
{
"predicate": "dgraph.graphql.schema",
"type": "string"
Expand Down Expand Up @@ -2231,6 +2241,17 @@ func TestSchemaQueryWithACL(t *testing.T) {
],
"name": "dgraph.graphql.history"
},
{
"fields": [
{
"name": "dgraph.graphql.p_query"
},
{
"name": "dgraph.graphql.p_sha256hash"
}
],
"name": "dgraph.graphql.persisted_query"
},
{
"fields": [
{
Expand Down Expand Up @@ -2289,6 +2310,10 @@ func TestSchemaQueryWithACL(t *testing.T) {
"fields": [],
"name": "dgraph.graphql.history"
},
{
"fields":[],
"name":"dgraph.graphql.persisted_query"
},
{
"fields": [],
"name": "dgraph.type.Group"
Expand Down Expand Up @@ -3221,7 +3246,6 @@ func TestDeleteGrootUserShouldFail(t *testing.T) {
})
require.NoError(t, err, "login failed")


resp := deleteUser(t, accessJwt, "groot", false)
require.Contains(t, resp.Errors.Error(),
"guardians group and groot user cannot be deleted.")
Expand Down Expand Up @@ -3252,12 +3276,12 @@ func TestDeleteGrootAndGuardiansUsingDelNQuadShouldFail(t *testing.T) {
grootUid, guardiansUid := getGrootAndGuardiansUid(t, dg)

// Try deleting groot user
_, err = deleteUsingNQuad(dg, "<" + grootUid + ">", "*", "*")
_, err = deleteUsingNQuad(dg, "<"+grootUid+">", "*", "*")
require.Error(t, err, "Deleting groot user should have returned an error")
require.Contains(t, err.Error(), "Properties of guardians group and groot user cannot be deleted")

// Try deleting guardians group
_, err = deleteUsingNQuad(dg, "<" + guardiansUid + ">", "*", "*")
_, err = deleteUsingNQuad(dg, "<"+guardiansUid+">", "*", "*")
require.Error(t, err, "Deleting guardians group should have returned an error")
require.Contains(t, err.Error(), "Properties of guardians group and groot user cannot be deleted")
}
Expand Down Expand Up @@ -3291,7 +3315,7 @@ func TestDropAllShouldResetGuardiansAndGroot(t *testing.T) {
// Try Drop All
op := api.Operation{
DropAll: true,
DropOp: api.Operation_ALL,
DropOp: api.Operation_ALL,
}
if err := dg.Alter(ctx, &op); err != nil {
t.Fatalf("Unable to drop all. Error:%v", err)
Expand All @@ -3302,7 +3326,7 @@ func TestDropAllShouldResetGuardiansAndGroot(t *testing.T) {

// Try Drop Data
op = api.Operation{
DropOp: api.Operation_DATA,
DropOp: api.Operation_DATA,
}
if err := dg.Alter(ctx, &op); err != nil {
t.Fatalf("Unable to drop data. Error:%v", err)
Expand Down
8 changes: 8 additions & 0 deletions graphql/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ const (
created_at: DateTime! @dgraph(pred: "dgraph.graphql.schema_created_at")
}
"""
PersistedQuery contains the query and sha256hash of the query.
"""
type PersistedQuery @dgraph(type: "dgraph.graphql.persisted_query") {
query: String! @dgraph(pred: "dgraph.graphql.p_query")
sha256Hash: String! @id @dgraph(pred: "dgraph.graphql.p_sha256hash")
}
"""
A NodeState is the state of an individual node in the Dgraph cluster.
"""
Expand Down
Loading

0 comments on commit 3b44d3f

Please sign in to comment.