From 222f42a21fb4d85200101ae6421a0efb08c76746 Mon Sep 17 00:00:00 2001 From: Arijit Das Date: Fri, 10 Jul 2020 18:22:45 +0530 Subject: [PATCH] perf(GraphQL): Nested auth queries no longer search through all possible records (#5666) Previously auth queries had type filter for all levels and hence it would fetch all nodes of a particular type. This PR only applies auth on the nodes that are part of the parent query. Hence, touching same/less number of nodes as it would have in the query without auth rules. --- graphql/admin/update_group.go | 4 +- graphql/e2e/auth/add_mutation_test.go | 10 +- graphql/e2e/auth/auth_test.go | 105 ++++++++- graphql/e2e/auth/schema.graphql | 4 +- graphql/resolve/auth_add_test.yaml | 103 +++++---- graphql/resolve/auth_delete_test.yaml | 115 +++++----- graphql/resolve/auth_query_test.yaml | 300 +++++++++++++++++++++----- graphql/resolve/auth_test.go | 31 ++- graphql/resolve/auth_update_test.yaml | 81 ++++--- graphql/resolve/mutation.go | 4 +- graphql/resolve/mutation_rewriter.go | 79 +++---- graphql/resolve/query_rewriter.go | 178 +++++++++++---- 12 files changed, 707 insertions(+), 307 deletions(-) diff --git a/graphql/admin/update_group.go b/graphql/admin/update_group.go index 4f060812f4d..8b071e8520a 100644 --- a/graphql/admin/update_group.go +++ b/graphql/admin/update_group.go @@ -50,7 +50,7 @@ func (urw *updateGroupRewriter) Rewrite( } for _, ruleI := range rules { rule := ruleI.(map[string]interface{}) - variable := varGen.Next(ruleType, "", "") + variable := varGen.Next(ruleType, "", "", false) predicate := rule["predicate"] permission := rule["permission"] @@ -96,7 +96,7 @@ func (urw *updateGroupRewriter) Rewrite( continue } - variable := varGen.Next(ruleType, "", "") + variable := varGen.Next(ruleType, "", "", false) addAclRuleQuery(upsertQuery, predicate.(string), variable) deleteJson := []byte(fmt.Sprintf(`[ diff --git a/graphql/e2e/auth/add_mutation_test.go b/graphql/e2e/auth/add_mutation_test.go index 8cf29bbcece..b89ac9d6cde 100644 --- a/graphql/e2e/auth/add_mutation_test.go +++ b/graphql/e2e/auth/add_mutation_test.go @@ -179,11 +179,11 @@ func TestAddDeepFilter(t *testing.T) { mutation addColumn($column: AddColumnInput!) { addColumn(input: [$column]) { column { - name - inProject { - projID - name - } + name + inProject { + projID + name + } } } } diff --git a/graphql/e2e/auth/auth_test.go b/graphql/e2e/auth/auth_test.go index 5aec83d3364..cb32682895c 100644 --- a/graphql/e2e/auth/auth_test.go +++ b/graphql/e2e/auth/auth_test.go @@ -135,6 +135,38 @@ type uidResult struct { } } +func (r *Region) add(t *testing.T, user, role string) { + getParams := &common.GraphQLParams{ + Headers: getJWT(t, user, role), + Query: ` + mutation addRegion($region: AddRegionInput!) { + addRegion(input: [$region]) { + numUids + } + } + `, + Variables: map[string]interface{}{"region": r}, + } + gqlResponse := getParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) +} + +func (r *Region) delete(t *testing.T, user, role string) { + getParams := &common.GraphQLParams{ + Headers: getJWT(t, user, role), + Query: ` + mutation deleteRegion($name: String) { + deleteRegion(filter:{name: { eq: $name}}) { + msg + } + } + `, + Variables: map[string]interface{}{"name": r.Name}, + } + gqlResponse := getParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) +} + func getJWT(t *testing.T, user, role string) http.Header { metaInfo.AuthVars = map[string]interface{}{} if user != "" { @@ -153,6 +185,57 @@ func getJWT(t *testing.T, user, role string) http.Header { return h } +func TestOptimizedNestedAuthQuery(t *testing.T) { + query := ` + query { + queryMovie { + content + regionsAvailable { + name + global + } + } + } + ` + user := "user1" + role := "ADMIN" + + getUserParams := &common.GraphQLParams{ + Headers: getJWT(t, user, role), + Query: query, + } + + gqlResponse := getUserParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + beforeTouchUids := gqlResponse.Extensions["touched_uids"] + beforeResult := gqlResponse.Data + + // Previously, Auth queries would have touched all the new `Regions`. But after the optimization + // we should only touch necessary `Regions` which are assigned to some `Movie`. Hence, adding + // these extra `Regions` would not increase the `touched_uids`. + var regions []Region + for i := 0; i < 100; i++ { + r := Region{ + Name: fmt.Sprintf("Test_Region_%d", i), + Global: true, + } + r.add(t, user, role) + regions = append(regions, r) + } + + gqlResponse = getUserParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + + afterTouchUids := gqlResponse.Extensions["touched_uids"] + require.Equal(t, beforeTouchUids, afterTouchUids) + require.Equal(t, beforeResult, gqlResponse.Data) + + // Clean up + for _, region := range regions { + region.delete(t, user, role) + } +} + func (s Student) deleteByEmail(t *testing.T) { getParams := &common.GraphQLParams{ Query: ` @@ -625,14 +708,14 @@ func TestDeepRBACValue(t *testing.T) { } query := ` -{ - queryUser (filter:{username:{eq:"user1"}}) { - username - issues { - msg - } - } -} + query { + queryUser (filter:{username:{eq:"user1"}}) { + username + issues { + msg + } + } + } ` for _, tcase := range testCases { @@ -658,7 +741,7 @@ func TestRBACFilter(t *testing.T) { query := ` query { - queryLog (order: {asc: logs}) { + queryLog (order: {asc: logs}) { logs } } @@ -803,8 +886,8 @@ func TestNestedFilter(t *testing.T) { }} query := ` - query { - queryMovie (order: {asc: content}) { + query { + queryMovie (order: {asc: content}) { content regionsAvailable (order: {asc: name}) { name diff --git a/graphql/e2e/auth/schema.graphql b/graphql/e2e/auth/schema.graphql index 0e6428aa8aa..403aa736264 100644 --- a/graphql/e2e/auth/schema.graphql +++ b/graphql/e2e/auth/schema.graphql @@ -68,7 +68,7 @@ type Region @auth( """} ){ id: ID! - name: String + name: String @search(by: [hash]) global: Boolean @search users: [User] } @@ -192,7 +192,7 @@ type Movie @auth( ]} ) { id: ID! - content: String + content: String @search(by: [hash]) hidden: Boolean @search regionsAvailable: [Region] reviews: [Review] diff --git a/graphql/resolve/auth_add_test.yaml b/graphql/resolve/auth_add_test.yaml index 26d55de8c4d..177a82ba8ef 100644 --- a/graphql/resolve/auth_add_test.yaml +++ b/graphql/resolve/auth_add_test.yaml @@ -17,11 +17,11 @@ { "UserSecret1": "0x123" } authquery: |- query { - UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { uid } UserSecret1 as var(func: uid(0x123)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | { "UserSecret": [ { "uid": "0x123" }] } @@ -46,11 +46,11 @@ { "UserSecret1": "0x123", "UserSecret2": "0x456" } authquery: |- query { - UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { uid } UserSecret1 as var(func: uid(0x123, 0x456)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | { "UserSecret": [ { "uid": "0x123" }, { "uid": "0x456" } ] } @@ -74,11 +74,11 @@ { "UserSecret1": "0x123" } authquery: |- query { - UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { uid } UserSecret1 as var(func: uid(0x123)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | { "UserSecret": [ ] } @@ -105,11 +105,11 @@ { "UserSecret1": "0x123", "UserSecret2": "0x456" } authquery: |- query { - UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { uid } UserSecret1 as var(func: uid(0x123, 0x456)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | { "UserSecret": [ { "uid": "0x123" }] } @@ -144,11 +144,11 @@ { "Column1": "0x456", "Ticket3": "0x789" } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -158,11 +158,11 @@ } dgraph.uid : uid } - Ticket(func: uid(Ticket3)) @filter(uid(Ticket4)) { + Ticket(func: uid(Ticket3)) @filter(uid(TicketAuth4)) { uid } Ticket3 as var(func: uid(0x789)) - Ticket4 as var(func: uid(Ticket3)) @cascade { + TicketAuth4 as var(func: uid(Ticket3)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -193,7 +193,8 @@ } variables: | { "col": - { "inProject": { "projID": "0x123" }, + { + "inProject": { "projID": "0x123" }, "name": "a column", "tickets": [ { "title": "a ticket" } ] } @@ -210,11 +211,11 @@ { "Column1": "0x456", "Ticket3": "0x789" } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -224,11 +225,11 @@ } dgraph.uid : uid } - Ticket(func: uid(Ticket3)) @filter(uid(Ticket4)) { + Ticket(func: uid(Ticket3)) @filter(uid(TicketAuth4)) { uid } Ticket3 as var(func: uid(0x789)) - Ticket4 as var(func: uid(Ticket3)) @cascade { + TicketAuth4 as var(func: uid(Ticket3)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -243,9 +244,7 @@ } } authjson: | - { - "Ticket": [ { "uid": "0x789" } ] - } + { "Ticket": [ { "uid": "0x789" } ]} error: { "message": "mutation failed because authorization failed" } @@ -288,11 +287,11 @@ { "Column1": "0x456", "Ticket3": "0x789", "Column4": "0x459", "Ticket6": "0x799" } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456, 0x459)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -302,11 +301,11 @@ } dgraph.uid : uid } - Ticket(func: uid(Ticket3)) @filter(uid(Ticket4)) { + Ticket(func: uid(Ticket3)) @filter(uid(TicketAuth4)) { uid } Ticket3 as var(func: uid(0x789, 0x799)) - Ticket4 as var(func: uid(Ticket3)) @cascade { + TicketAuth4 as var(func: uid(Ticket3)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -365,11 +364,11 @@ { "Column1": "0x456", "Ticket3": "0x789", "Column4": "0x459", "Ticket6": "0x799" } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456, 0x459)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -379,11 +378,11 @@ } dgraph.uid : uid } - Ticket(func: uid(Ticket3)) @filter(uid(Ticket4)) { + Ticket(func: uid(Ticket3)) @filter(uid(TicketAuth4)) { uid } Ticket3 as var(func: uid(0x789, 0x799)) - Ticket4 as var(func: uid(Ticket3)) @cascade { + TicketAuth4 as var(func: uid(Ticket3)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -440,10 +439,10 @@ Column4(func: uid(Column4)) { uid } - Column4.auth(func: uid(Column4)) @filter(uid(Column5)) { + Column4.auth(func: uid(Column4)) @filter(uid(ColumnAuth5)) { uid } - Column5 as var(func: uid(Column4)) @cascade { + ColumnAuth5 as var(func: uid(Column4)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -465,11 +464,11 @@ { "Column1": "0x456" } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -515,10 +514,10 @@ Column4(func: uid(Column4)) { uid } - Column4.auth(func: uid(Column4)) @filter(uid(Column5)) { + Column4.auth(func: uid(Column4)) @filter(uid(ColumnAuth5)) { uid } - Column5 as var(func: uid(Column4)) @cascade { + ColumnAuth5 as var(func: uid(Column4)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -539,11 +538,11 @@ { "Column1": "0x456" } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -591,10 +590,10 @@ Column4(func: uid(Column4)) { uid } - Column4.auth(func: uid(Column4)) @filter(uid(Column5)) { + Column4.auth(func: uid(Column4)) @filter(uid(ColumnAuth5)) { uid } - Column5 as var(func: uid(Column4)) @cascade { + ColumnAuth5 as var(func: uid(Column4)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -618,11 +617,11 @@ } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -632,11 +631,11 @@ } dgraph.uid : uid } - Project(func: uid(Project3)) @filter(uid(Project4)) { + Project(func: uid(Project3)) @filter(uid(ProjectAuth4)) { uid } Project3 as var(func: uid(0x123)) - Project4 as var(func: uid(Project3)) @cascade { + ProjectAuth4 as var(func: uid(Project3)) @cascade { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid @@ -680,10 +679,10 @@ Column4(func: uid(Column4)) { uid } - Column4.auth(func: uid(Column4)) @filter(uid(Column5)) { + Column4.auth(func: uid(Column4)) @filter(uid(ColumnAuth5)) { uid } - Column5 as var(func: uid(Column4)) @cascade { + ColumnAuth5 as var(func: uid(Column4)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -706,11 +705,11 @@ } authquery: |- query { - Column(func: uid(Column1)) @filter(uid(Column2)) { + Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { uid } Column1 as var(func: uid(0x456)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -720,11 +719,11 @@ } dgraph.uid : uid } - Project(func: uid(Project3)) @filter(uid(Project4)) { + Project(func: uid(Project3)) @filter(uid(ProjectAuth4)) { uid } Project3 as var(func: uid(0x123)) - Project4 as var(func: uid(Project3)) @cascade { + ProjectAuth4 as var(func: uid(Project3)) @cascade { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid @@ -825,11 +824,11 @@ } authquery: |- query { - Project(func: uid(Project1)) @filter(uid(Project2)) { + Project(func: uid(Project1)) @filter(uid(ProjectAuth2)) { uid } Project1 as var(func: uid(0x123)) - Project2 as var(func: uid(Project1)) @cascade { + ProjectAuth2 as var(func: uid(Project1)) @cascade { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid @@ -880,11 +879,11 @@ { "Issue1": "0x789" } authquery: |- query { - Issue(func: uid(Issue1)) @filter(uid(Issue2)) { + Issue(func: uid(Issue1)) @filter(uid(IssueAuth2)) { uid } Issue1 as var(func: uid(0x789)) - Issue2 as var(func: uid(Issue1)) @cascade { + IssueAuth2 as var(func: uid(Issue1)) @cascade { owner : Issue.owner @filter(eq(User.username, "user1")) dgraph.uid : uid } diff --git a/graphql/resolve/auth_delete_test.yaml b/graphql/resolve/auth_delete_test.yaml index e35535ef329..14370f9311b 100644 --- a/graphql/resolve/auth_delete_test.yaml +++ b/graphql/resolve/auth_delete_test.yaml @@ -14,11 +14,12 @@ ] dgquery: |- query { - x as deleteUserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + x as deleteUserSecret(func: uid(UserSecretRoot)) { uid } + UserSecretRoot as var(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: type(UserSecret)) @filter(anyofterms(UserSecret.aSecret, "auth is applied")) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } - name: "Delete with deep auth" @@ -45,13 +46,14 @@ ] dgquery: |- query { - x as deleteTicket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + x as deleteTicket(func: uid(TicketRoot)) { uid Column3 as Ticket.onColumn User4 as Ticket.assignedTo } + TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) Ticket1 as var(func: type(Ticket)) @filter(anyofterms(Ticket.title, "auth is applied")) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -85,12 +87,6 @@ } } } - assignedTo { - username - age - isPublic - disabled - } } } } @@ -111,13 +107,14 @@ ] dgquery: |- query { - x as deleteTicket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + x as deleteTicket(func: uid(TicketRoot)) { uid Column3 as Ticket.onColumn User4 as Ticket.assignedTo } + TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) Ticket1 as var(func: type(Ticket)) @filter(anyofterms(Ticket.title, "auth is applied")) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -130,12 +127,12 @@ } dgraph.uid : uid } - ticket(func: uid(x)) @filter(uid(Ticket8)) { + ticket(func: uid(Ticket5)) { title : Ticket.title - onColumn : Ticket.onColumn @filter(uid(Column6)) { - inProject : Column.inProject @filter(uid(Project5)) { - roles : Project.roles { - assignedTo : Role.assignedTo { + onColumn : Ticket.onColumn @filter(uid(Column15)) { + inProject : Column.inProject @filter(uid(Project13)) { + roles : Project.roles @filter(uid(Role11)) { + assignedTo : Role.assignedTo @filter(uid(User10)) { username : User.username age : User.age dgraph.uid : uid @@ -146,17 +143,11 @@ } dgraph.uid : uid } - assignedTo : Ticket.assignedTo { - username : User.username - age : User.age - isPublic : User.isPublic - disabled : User.disabled - dgraph.uid : uid - } dgraph.uid : uid } - Ticket7 as var(func: type(Ticket)) - Ticket8 as var(func: uid(Ticket7)) @cascade { + Ticket5 as var(func: uid(Ticket16)) @filter(uid(TicketAuth17)) + Ticket16 as var(func: uid(x)) + TicketAuth17 as var(func: uid(Ticket16)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -169,14 +160,30 @@ } dgraph.uid : uid } - Project5 as var(func: type(Project)) @cascade { + var(func: uid(Ticket5)) { + Column6 as Ticket.onColumn + } + Column15 as var(func: uid(Column6)) @filter(uid(ColumnAuth14)) + var(func: uid(Column6)) { + Project7 as Column.inProject + } + Project13 as var(func: uid(Project7)) @filter(uid(ProjectAuth12)) + var(func: uid(Project7)) { + Role8 as Project.roles + } + Role11 as var(func: uid(Role8)) + var(func: uid(Role8)) { + User9 as Role.assignedTo + } + User10 as var(func: uid(User9)) + ProjectAuth12 as var(func: uid(Project7)) @cascade { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid } dgraph.uid : uid } - Column6 as var(func: type(Column)) @cascade { + ColumnAuth14 as var(func: uid(Column6)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -192,12 +199,8 @@ gqlquery: | mutation($projs: [ID!]) { deleteProject (filter: { projID: $projs}) { - numUids - project { - name - random + numUids } - } } variables: | { @@ -222,11 +225,6 @@ uid Column2 as Project.columns } - project(func: uid(x)) { - name : Project.name - random : Project.random - dgraph.uid : uid - } } - name: "Delete with top level RBAC false." @@ -282,13 +280,14 @@ ] dgquery: |- query { - x as deleteUser(func: uid(User1)) @filter((uid(User2) AND uid(User3))) { + x as deleteUser(func: uid(UserRoot)) { uid Ticket4 as User.tickets } + UserRoot as var(func: uid(User1)) @filter((uid(UserAuth2) AND uid(UserAuth3))) User1 as var(func: type(User)) @filter(eq(User.username, "userxyz")) - User2 as var(func: uid(User1)) @filter(eq(User.username, "user1")) @cascade - User3 as var(func: uid(User1)) @filter(eq(User.isPublic, true)) @cascade + UserAuth2 as var(func: uid(User1)) @filter(eq(User.username, "user1")) @cascade + UserAuth3 as var(func: uid(User1)) @filter(eq(User.isPublic, true)) @cascade } - name: "Filtering by ID" @@ -311,11 +310,12 @@ ] dgquery: |- query { - x as deleteRegion(func: uid(Region1)) @filter(uid(Region2)) { + x as deleteRegion(func: uid(RegionRoot)) { uid } + RegionRoot as var(func: uid(Region1)) @filter(uid(RegionAuth2)) Region1 as var(func: uid(0x1, 0x2)) @filter(type(Region)) - Region2 as var(func: uid(Region1)) @filter(eq(Region.global, true)) @cascade + RegionAuth2 as var(func: uid(Region1)) @filter(eq(Region.global, true)) @cascade } - name: "Delete with top level RBAC false." @@ -347,6 +347,10 @@ mutation deleteLog($filter: LogFilter!) { deleteLog(filter: $filter) { msg + log (order: { asc: logs }) { + logs + random + } } } variables: | @@ -363,9 +367,18 @@ }] dgquery: |- query { - x as deleteLog(func: uid(0x1, 0x2)) @filter(type(Log)) { + x as deleteLog(func: uid(LogRoot)) { uid } + LogRoot as var(func: uid(Log1)) + Log1 as var(func: uid(0x1, 0x2)) @filter(type(Log)) + log(func: uid(Log2), orderasc: Log.logs) { + logs : Log.logs + random : Log.random + dgraph.uid : uid + } + Log2 as var(func: uid(Log3), orderasc: Log.logs) + Log3 as var(func: uid(x)) } - name: "Delete with top level OR RBAC true." @@ -385,9 +398,11 @@ [{ "uid": "uid(x)" }] dgquery: |- query { - x as deleteComplexLog(func: uid(0x1, 0x2)) @filter(type(ComplexLog)) { + x as deleteComplexLog(func: uid(ComplexLogRoot)) { uid } + ComplexLogRoot as var(func: uid(ComplexLog1)) + ComplexLog1 as var(func: uid(0x1, 0x2)) @filter(type(ComplexLog)) } - name: "Delete with top level OR RBAC false." @@ -406,11 +421,12 @@ [{ "uid": "uid(x)" }] dgquery: |- query { - x as deleteComplexLog(func: uid(ComplexLog1)) @filter(uid(ComplexLog2)) { + x as deleteComplexLog(func: uid(ComplexLogRoot)) { uid } + ComplexLogRoot as var(func: uid(ComplexLog1)) @filter(uid(ComplexLogAuth2)) ComplexLog1 as var(func: uid(0x1, 0x2)) @filter(type(ComplexLog)) - ComplexLog2 as var(func: uid(ComplexLog1)) @filter(eq(ComplexLog.visible, true)) @cascade + ComplexLogAuth2 as var(func: uid(ComplexLog1)) @filter(eq(ComplexLog.visible, true)) @cascade } - name: "Delete with top level AND RBAC true." @@ -434,11 +450,12 @@ }] dgquery: |- query { - x as deleteIssue(func: uid(Issue1)) @filter(uid(Issue2)) { + x as deleteIssue(func: uid(IssueRoot)) { uid } + IssueRoot as var(func: uid(Issue1)) @filter(uid(IssueAuth2)) Issue1 as var(func: type(Issue)) - Issue2 as var(func: uid(Issue1)) @cascade { + IssueAuth2 as var(func: uid(Issue1)) @cascade { owner : Issue.owner @filter(eq(User.username, "user1")) dgraph.uid : uid } diff --git a/graphql/resolve/auth_query_test.yaml b/graphql/resolve/auth_query_test.yaml index 9b796e237f0..ba4b50bb28b 100644 --- a/graphql/resolve/auth_query_test.yaml +++ b/graphql/resolve/auth_query_test.yaml @@ -8,12 +8,13 @@ role: "ADMIN" dgquery: |- query { - queryStudent(func: uid(Student1)) @filter(uid(Student2)) { + queryStudent(func: uid(StudentRoot)) { email : IOw80vnV dgraph.uid : uid } + StudentRoot as var(func: uid(Student1)) @filter(uid(StudentAuth2)) Student1 as var(func: type(is7sowSm)) - Student2 as var(func: uid(Student1)) @filter(eq(IOw80vnV, "user1")) @cascade + StudentAuth2 as var(func: uid(Student1)) @filter(eq(IOw80vnV, "user1")) @cascade } - name: "Auth query with @dgraph pred (Test RBAC)." @@ -42,22 +43,27 @@ } dgquery: |- query { - getProject(func: uid(Project2)) @filter((uid(Project3) AND type(Project))) { + getProject(func: uid(ProjectRoot)) @filter(type(Project)) { projID : uid - columns : Project.columns @filter(uid(Column1)) { + columns : Project.columns @filter(uid(Column3)) { name : Column.name colID : uid } } - Project2 as var(func: uid(0x123)) - Project3 as var(func: uid(Project2)) @cascade { + ProjectRoot as var(func: uid(Project4)) @filter(uid(ProjectAuth5)) + Project4 as var(func: uid(0x123)) + ProjectAuth5 as var(func: uid(Project4)) @cascade { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid } dgraph.uid : uid } - Column1 as var(func: type(Column)) @cascade { + var(func: uid(ProjectRoot)) { + Column1 as Project.columns + } + Column3 as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -79,12 +85,13 @@ } dgquery: |- query { - queryUserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + queryUserSecret(func: uid(UserSecretRoot)) { id : uid ownedBy : UserSecret.ownedBy } + UserSecretRoot as var(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: type(UserSecret)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } - name: "Auth with top level filter : get" @@ -97,12 +104,13 @@ } dgquery: |- query { - getUserSecret(func: uid(UserSecret1)) @filter((uid(UserSecret2) AND type(UserSecret))) { + getUserSecret(func: uid(UserSecretRoot)) @filter(type(UserSecret)) { id : uid ownedBy : UserSecret.ownedBy } + UserSecretRoot as var(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: uid(0x123)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } - name: "Auth with top level filter : query and filter" @@ -115,12 +123,13 @@ } dgquery: |- query { - queryUserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + queryUserSecret(func: uid(UserSecretRoot)) { id : uid ownedBy : UserSecret.ownedBy } + UserSecretRoot as var(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: type(UserSecret)) @filter(eq(UserSecret.ownedBy, "user2")) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } - name: "Deep RBAC rules true" @@ -135,13 +144,19 @@ role: "ADMIN" dgquery: |- query { - queryUser(func: type(User)) { - issues : User.issues @filter(uid(Issue1)) { + queryUser(func: uid(UserRoot)) { + issues : User.issues @filter(uid(Issue3)) { id : uid } dgraph.uid : uid } - Issue1 as var(func: type(Issue)) @cascade { + UserRoot as var(func: uid(User4)) + User4 as var(func: type(User)) + var(func: uid(UserRoot)) { + Issue1 as User.issues + } + Issue3 as var(func: uid(Issue1)) @filter(uid(IssueAuth2)) + IssueAuth2 as var(func: uid(Issue1)) @cascade { owner : Issue.owner @filter(eq(User.username, "user1")) dgraph.uid : uid } @@ -160,10 +175,12 @@ role: "USER" dgquery: |- query { - queryUser(func: type(User)) { + queryUser(func: uid(UserRoot)) { username : User.username dgraph.uid : uid } + UserRoot as var(func: uid(User2)) + User2 as var(func: type(User)) } @@ -176,17 +193,18 @@ } role: "ADMIN" dgquery: |- - query { - queryIssue(func: uid(Issue1)) @filter(uid(Issue2)) { - msg : Issue.msg - dgraph.uid : uid - } - Issue1 as var(func: type(Issue)) - Issue2 as var(func: uid(Issue1)) @cascade { - owner : Issue.owner @filter(eq(User.username, "user1")) - dgraph.uid : uid - } - } + query { + queryIssue(func: uid(IssueRoot)) { + msg : Issue.msg + dgraph.uid : uid + } + IssueRoot as var(func: uid(Issue1)) @filter(uid(IssueAuth2)) + Issue1 as var(func: type(Issue)) + IssueAuth2 as var(func: uid(Issue1)) @cascade { + owner : Issue.owner @filter(eq(User.username, "user1")) + dgraph.uid : uid + } + } - name: "Auth with complex rbac rules, true" gqlquery: | @@ -198,10 +216,12 @@ role: "ADMIN" dgquery: |- query { - queryComplexLog(func: type(ComplexLog)) { + queryComplexLog(func: uid(ComplexLogRoot)) { logs : ComplexLog.logs dgraph.uid : uid } + ComplexLogRoot as var(func: uid(ComplexLog1)) + ComplexLog1 as var(func: type(ComplexLog)) } - name: "Auth with complex rbac rules, false" @@ -227,10 +247,12 @@ role: "ADMIN" dgquery: |- query { - queryLog(func: type(Log)) { + queryLog(func: uid(LogRoot)) { logs : Log.logs dgraph.uid : uid } + LogRoot as var(func: uid(Log1)) + Log1 as var(func: type(Log)) } - name: "Auth with top level rbac false" @@ -270,10 +292,12 @@ role: "ADMIN" dgquery: |- query { - queryProject(func: type(Project)) { + queryProject(func: uid(ProjectRoot)) { name : Project.name dgraph.uid : uid } + ProjectRoot as var(func: uid(Project1)) + Project1 as var(func: type(Project)) } - name: "Query with missing jwt variables" @@ -285,15 +309,16 @@ } dgquery: |- query { - queryGroup(func: uid(Group1)) @filter((uid(Group2) OR uid(Group3))) { + queryGroup(func: uid(GroupRoot)) { id : uid } + GroupRoot as var(func: uid(Group1)) @filter((uid(GroupAuth2) OR uid(GroupAuth3))) Group1 as var(func: type(Group)) - Group2 as var(func: uid(Group1)) @cascade { + GroupAuth2 as var(func: uid(Group1)) @cascade { users : Group.users @filter(eq(User.username, "user1")) dgraph.uid : uid } - Group3 as var(func: uid(Group1)) @cascade { + GroupAuth3 as var(func: uid(Group1)) @cascade { createdBy : Group.createdBy @filter(eq(User.username, "user1")) dgraph.uid : uid } @@ -301,7 +326,7 @@ - name: "Auth with top level OR rbac false" gqlquery: | - query { + query { queryProject { name } @@ -309,12 +334,13 @@ role: "USER" dgquery: |- query { - queryProject(func: uid(Project1)) @filter(uid(Project2)) { + queryProject(func: uid(ProjectRoot)) { name : Project.name dgraph.uid : uid } + ProjectRoot as var(func: uid(Project1)) @filter(uid(ProjectAuth2)) Project1 as var(func: type(Project)) - Project2 as var(func: uid(Project1)) @cascade { + ProjectAuth2 as var(func: uid(Project1)) @cascade { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid @@ -333,12 +359,13 @@ } dgquery: |- query { - queryUserSecret(func: uid(UserSecret1), orderasc: UserSecret.aSecret, first: 1) @filter(uid(UserSecret2)) { + queryUserSecret(func: uid(UserSecretRoot), orderasc: UserSecret.aSecret) { id : uid ownedBy : UserSecret.ownedBy } + UserSecretRoot as var(func: uid(UserSecret1), orderasc: UserSecret.aSecret, first: 1) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: type(UserSecret)) @filter(eq(UserSecret.ownedBy, "user2")) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } - name: "Auth with deep filter : query top-level" @@ -351,12 +378,13 @@ } dgquery: |- query { - queryTicket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + queryTicket(func: uid(TicketRoot)) { id : uid title : Ticket.title } + TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) Ticket1 as var(func: type(Ticket)) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -384,15 +412,21 @@ } dgquery: |- query { - queryUser(func: type(User)) { + queryUser(func: uid(UserRoot)) { username : User.username - tickets : User.tickets @filter(uid(Ticket1)) { + tickets : User.tickets @filter(uid(Ticket3)) { id : uid title : Ticket.title } dgraph.uid : uid } - Ticket1 as var(func: type(Ticket)) @cascade { + UserRoot as var(func: uid(User4)) + User4 as var(func: type(User)) + var(func: uid(UserRoot)) { + Ticket1 as User.tickets + } + Ticket3 as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -420,15 +454,21 @@ } dgquery: |- query { - queryUser(func: type(User)) { + queryUser(func: uid(UserRoot)) { username : User.username - tickets : User.tickets @filter((anyofterms(Ticket.title, "graphql") AND uid(Ticket1))) { + tickets : User.tickets @filter(uid(Ticket3)) { id : uid title : Ticket.title } dgraph.uid : uid } - Ticket1 as var(func: type(Ticket)) @cascade { + UserRoot as var(func: uid(User4)) + User4 as var(func: type(User)) + var(func: uid(UserRoot)) { + Ticket1 as User.tickets + } + Ticket3 as var(func: uid(Ticket1)) @filter((anyofterms(Ticket.title, "graphql") AND uid(TicketAuth2))) + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -443,6 +483,148 @@ } } +- name: "Auth deep query - 0 level" + gqlquery: | + query { + queryMovie(filter: { content: { eq: "A. N. Author" } }, order: { asc: content }, first: 10, offset: 10) { + content + } + } + dgquery: |- + query { + queryMovie(func: uid(MovieRoot), orderasc: Movie.content) { + content : Movie.content + dgraph.uid : uid + } + MovieRoot as var(func: uid(Movie1), orderasc: Movie.content, first: 10, offset: 10) @filter((NOT (uid(MovieAuth2)) AND (uid(MovieAuth3) OR uid(MovieAuth4)))) + Movie1 as var(func: type(Movie)) @filter(eq(Movie.content, "A. N. Author")) + MovieAuth2 as var(func: uid(Movie1)) @filter(eq(Movie.hidden, true)) @cascade + MovieAuth3 as var(func: uid(Movie1)) @cascade { + regionsAvailable : Movie.regionsAvailable { + users : Region.users @filter(eq(User.username, "user1")) + dgraph.uid : uid + } + dgraph.uid : uid + } + MovieAuth4 as var(func: uid(Movie1)) @cascade { + regionsAvailable : Movie.regionsAvailable @filter(eq(Region.global, true)) + dgraph.uid : uid + } + } + +- name: "Auth deep query - 1 level" + gqlquery: | + query { + queryMovie(filter: { content: { eq: "MovieXYZ" } }, order: { asc: content }, first: 10, offset: 10) @cascade { + content + regionsAvailable(filter: { name: { eq: "Region123" } }, order: { asc: name }, first: 10, offset: 10) { + name + global + } + } + } + dgquery: |- + query { + queryMovie(func: uid(MovieRoot), orderasc: Movie.content) @cascade { + content : Movie.content + regionsAvailable : Movie.regionsAvailable @filter(uid(Region2)) (orderasc: Region.name) { + name : Region.name + global : Region.global + dgraph.uid : uid + } + dgraph.uid : uid + } + MovieRoot as var(func: uid(Movie3), orderasc: Movie.content, first: 10, offset: 10) @filter((NOT (uid(MovieAuth4)) AND (uid(MovieAuth5) OR uid(MovieAuth6)))) + Movie3 as var(func: type(Movie)) @filter(eq(Movie.content, "MovieXYZ")) + MovieAuth4 as var(func: uid(Movie3)) @filter(eq(Movie.hidden, true)) @cascade + MovieAuth5 as var(func: uid(Movie3)) @cascade { + regionsAvailable : Movie.regionsAvailable { + users : Region.users @filter(eq(User.username, "user1")) + dgraph.uid : uid + } + dgraph.uid : uid + } + MovieAuth6 as var(func: uid(Movie3)) @cascade { + regionsAvailable : Movie.regionsAvailable @filter(eq(Region.global, true)) + dgraph.uid : uid + } + var(func: uid(MovieRoot)) { + Region1 as Movie.regionsAvailable + } + Region2 as var(func: uid(Region1), orderasc: Region.name, first: 10, offset: 10) @filter(eq(Region.name, "Region123")) + } + +- name: "Auth deep query - 3 level" + gqlquery: | + query { + queryMovie(filter: { content: { eq: "MovieXYZ" } }, order: { asc: content }, first: 10, offset: 10) { + content + regionsAvailable(filter: { name: { eq: "Region123" } }, order: { asc: name }, first: 10, offset: 10) @cascade { + name + global + users(filter: { username: { eq: "User321" } }, order: { asc: username }, first: 10, offset: 10) { + username + age + isPublic + secrets(filter: { aSecret: { allofterms : "Secret132" } }, order: { asc: aSecret }, first: 10, offset: 10) { + aSecret + ownedBy + } + } + } + } + } + dgquery: |- + query { + queryMovie(func: uid(MovieRoot), orderasc: Movie.content) { + content : Movie.content + regionsAvailable : Movie.regionsAvailable @filter(uid(Region7)) (orderasc: Region.name) @cascade { + name : Region.name + global : Region.global + users : Region.users @filter(uid(User6)) (orderasc: User.username) { + username : User.username + age : User.age + isPublic : User.isPublic + secrets : User.secrets @filter(uid(UserSecret5)) (orderasc: UserSecret.aSecret) { + aSecret : UserSecret.aSecret + ownedBy : UserSecret.ownedBy + dgraph.uid : uid + } + dgraph.uid : uid + } + dgraph.uid : uid + } + dgraph.uid : uid + } + MovieRoot as var(func: uid(Movie8), orderasc: Movie.content, first: 10, offset: 10) @filter((NOT (uid(MovieAuth9)) AND (uid(MovieAuth10) OR uid(MovieAuth11)))) + Movie8 as var(func: type(Movie)) @filter(eq(Movie.content, "MovieXYZ")) + MovieAuth9 as var(func: uid(Movie8)) @filter(eq(Movie.hidden, true)) @cascade + MovieAuth10 as var(func: uid(Movie8)) @cascade { + regionsAvailable : Movie.regionsAvailable { + users : Region.users @filter(eq(User.username, "user1")) + dgraph.uid : uid + } + dgraph.uid : uid + } + MovieAuth11 as var(func: uid(Movie8)) @cascade { + regionsAvailable : Movie.regionsAvailable @filter(eq(Region.global, true)) + dgraph.uid : uid + } + var(func: uid(MovieRoot)) { + Region1 as Movie.regionsAvailable + } + Region7 as var(func: uid(Region1), orderasc: Region.name, first: 10, offset: 10) @filter(eq(Region.name, "Region123")) + var(func: uid(Region1)) { + User2 as Region.users + } + User6 as var(func: uid(User2), orderasc: User.username, first: 10, offset: 10) @filter(eq(User.username, "User321")) + var(func: uid(User2)) { + UserSecret3 as User.secrets + } + UserSecret5 as var(func: uid(UserSecret3), orderasc: UserSecret.aSecret, first: 10, offset: 10) @filter((allofterms(UserSecret.aSecret, "Secret132") AND uid(UserSecretAuth4))) + UserSecretAuth4 as var(func: uid(UserSecret3)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + } + - name: "Auth with complex filter" gqlquery: | query { @@ -452,20 +634,21 @@ } dgquery: |- query { - queryMovie(func: uid(Movie1)) @filter((NOT (uid(Movie2)) AND (uid(Movie3) OR uid(Movie4)))) { + queryMovie(func: uid(MovieRoot)) { content : Movie.content dgraph.uid : uid } + MovieRoot as var(func: uid(Movie1)) @filter((NOT (uid(MovieAuth2)) AND (uid(MovieAuth3) OR uid(MovieAuth4)))) Movie1 as var(func: type(Movie)) - Movie2 as var(func: uid(Movie1)) @filter(eq(Movie.hidden, true)) @cascade - Movie3 as var(func: uid(Movie1)) @cascade { + MovieAuth2 as var(func: uid(Movie1)) @filter(eq(Movie.hidden, true)) @cascade + MovieAuth3 as var(func: uid(Movie1)) @cascade { regionsAvailable : Movie.regionsAvailable { users : Region.users @filter(eq(User.username, "user1")) dgraph.uid : uid } dgraph.uid : uid } - Movie4 as var(func: uid(Movie1)) @cascade { + MovieAuth4 as var(func: uid(Movie1)) @cascade { regionsAvailable : Movie.regionsAvailable @filter(eq(Region.global, true)) dgraph.uid : uid } @@ -508,10 +691,12 @@ } dgquery: |- query { - queryUser(func: type(User)) { + queryUser(func: uid(UserRoot)) { username : User.username dgraph.uid : uid } + UserRoot as var(func: uid(User2)) + User2 as var(func: type(User)) } - name: "Query with missing variable - partial jwt token" @@ -524,10 +709,12 @@ role: "ADMIN" dgquery: |- query { - queryProject(func: type(Project)) { + queryProject(func: uid(ProjectRoot)) { name : Project.name dgraph.uid : uid } + ProjectRoot as var(func: uid(Project1)) + Project1 as var(func: type(Project)) } - name: "Query with missing jwt token - type without auth directive" @@ -554,13 +741,14 @@ } dgquery: |- query { - queryMovie(func: uid(Movie1)) @filter((NOT (uid(Movie2)) AND uid(Movie3))) { + queryMovie(func: uid(MovieRoot)) { content : Movie.content dgraph.uid : uid } + MovieRoot as var(func: uid(Movie1)) @filter((NOT (uid(MovieAuth2)) AND uid(MovieAuth3))) Movie1 as var(func: type(Movie)) - Movie2 as var(func: uid(Movie1)) @filter(eq(Movie.hidden, true)) @cascade - Movie3 as var(func: uid(Movie1)) @cascade { + MovieAuth2 as var(func: uid(Movie1)) @filter(eq(Movie.hidden, true)) @cascade + MovieAuth3 as var(func: uid(Movie1)) @cascade { regionsAvailable : Movie.regionsAvailable @filter(eq(Region.global, true)) dgraph.uid : uid } diff --git a/graphql/resolve/auth_test.go b/graphql/resolve/auth_test.go index d223af7ab01..0d8d3f260b7 100644 --- a/graphql/resolve/auth_test.go +++ b/graphql/resolve/auth_test.go @@ -193,7 +193,6 @@ func queryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta) { for _, tcase := range tests { t.Run(tcase.Name, func(t *testing.T) { - op, err := gqlSchema.Operation( &schema.Request{ Query: tcase.GQLQuery, @@ -249,16 +248,17 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet rewriter: NewAddRewriter, assigned: map[string]string{"Ticket1": "0x4"}, dgQuery: `query { - ticket(func: uid(Ticket2)) @filter(uid(Ticket3)) { + ticket(func: uid(TicketRoot)) { id : uid title : Ticket.title - onColumn : Ticket.onColumn @filter(uid(Column1)) { + onColumn : Ticket.onColumn @filter(uid(Column3)) { colID : uid name : Column.name } } - Ticket2 as var(func: uid(0x4)) - Ticket3 as var(func: uid(Ticket2)) @cascade { + TicketRoot as var(func: uid(Ticket4)) @filter(uid(TicketAuth5)) + Ticket4 as var(func: uid(0x4)) + TicketAuth5 as var(func: uid(Ticket4)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -271,7 +271,11 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet } dgraph.uid : uid } - Column1 as var(func: type(Column)) @cascade { + var(func: uid(TicketRoot)) { + Column1 as Ticket.onColumn + } + Column3 as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -300,16 +304,17 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet result: map[string]interface{}{ "updateTicket": []interface{}{map[string]interface{}{"uid": "0x4"}}}, dgQuery: `query { - ticket(func: uid(Ticket2)) @filter(uid(Ticket3)) { + ticket(func: uid(TicketRoot)) { id : uid title : Ticket.title - onColumn : Ticket.onColumn @filter(uid(Column1)) { + onColumn : Ticket.onColumn @filter(uid(Column3)) { colID : uid name : Column.name } } - Ticket2 as var(func: uid(0x4)) - Ticket3 as var(func: uid(Ticket2)) @cascade { + TicketRoot as var(func: uid(Ticket4)) @filter(uid(TicketAuth5)) + Ticket4 as var(func: uid(0x4)) + TicketAuth5 as var(func: uid(Ticket4)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -322,7 +327,11 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet } dgraph.uid : uid } - Column1 as var(func: type(Column)) @cascade { + var(func: uid(TicketRoot)) { + Column1 as Ticket.onColumn + } + Column3 as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) diff --git a/graphql/resolve/auth_update_test.yaml b/graphql/resolve/auth_update_test.yaml index 0187e68e6fe..a6ffaf54f17 100644 --- a/graphql/resolve/auth_update_test.yaml +++ b/graphql/resolve/auth_update_test.yaml @@ -15,11 +15,12 @@ } dgquery: |- query { - x as updateUserSecret(func: uid(UserSecret1)) @filter(uid(UserSecret2)) { + x as updateUserSecret(func: uid(UserSecretRoot)) { uid } + UserSecretRoot as var(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: type(UserSecret)) @filter(uid(0x123)) - UserSecret2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade + UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } uids: | { } @@ -45,11 +46,12 @@ } dgquery: |- query { - x as updateColumn(func: uid(Column1)) @filter(uid(Column2)) { + x as updateColumn(func: uid(ColumnRoot)) { uid } + ColumnRoot as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) Column1 as var(func: type(Column)) @filter(uid(0x123)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -66,11 +68,11 @@ { } authquery: |- query { - Ticket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + Ticket(func: uid(Ticket1)) @filter(uid(TicketAuth2)) { uid } Ticket1 as var(func: uid(0x789)) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -111,11 +113,12 @@ } dgquery: |- query { - x as updateColumn(func: uid(Column1)) @filter(uid(Column2)) { + x as updateColumn(func: uid(ColumnRoot)) { uid } + ColumnRoot as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) Column1 as var(func: type(Column)) @filter(uid(0x123)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -132,11 +135,11 @@ { } authquery: |- query { - Ticket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + Ticket(func: uid(Ticket1)) @filter(uid(TicketAuth2)) { uid } Ticket1 as var(func: uid(0x789)) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -180,11 +183,12 @@ } dgquery: |- query { - x as updateColumn(func: uid(Column1)) @filter(uid(Column2)) { + x as updateColumn(func: uid(ColumnRoot)) { uid } + ColumnRoot as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) Column1 as var(func: type(Column)) @filter(uid(0x123)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -203,10 +207,10 @@ Column5(func: uid(Column5)) { uid } - Column5.auth(func: uid(Column5)) @filter(uid(Column6)) { + Column5.auth(func: uid(Column5)) @filter(uid(ColumnAuth6)) { uid } - Column6 as var(func: uid(Column5)) @cascade { + ColumnAuth6 as var(func: uid(Column5)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -246,11 +250,12 @@ } dgquery: |- query { - x as updateColumn(func: uid(Column1)) @filter(uid(Column2)) { + x as updateColumn(func: uid(ColumnRoot)) { uid } + ColumnRoot as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) Column1 as var(func: type(Column)) @filter(uid(0x123)) - Column2 as var(func: uid(Column1)) @cascade { + ColumnAuth2 as var(func: uid(Column1)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -269,10 +274,10 @@ Column5(func: uid(Column5)) { uid } - Column5.auth(func: uid(Column5)) @filter(uid(Column6)) { + Column5.auth(func: uid(Column5)) @filter(uid(ColumnAuth6)) { uid } - Column6 as var(func: uid(Column5)) @cascade { + ColumnAuth6 as var(func: uid(Column5)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -313,11 +318,12 @@ } dgquery: |- query { - x as updateTicket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + x as updateTicket(func: uid(TicketRoot)) { uid } + TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) Ticket1 as var(func: type(Ticket)) @filter(uid(0x123)) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -339,10 +345,10 @@ Column5(func: uid(Column5)) { uid } - Column5.auth(func: uid(Column5)) @filter(uid(Column6)) { + Column5.auth(func: uid(Column5)) @filter(uid(ColumnAuth6)) { uid } - Column6 as var(func: uid(Column5)) @cascade { + ColumnAuth6 as var(func: uid(Column5)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -381,11 +387,12 @@ } dgquery: |- query { - x as updateTicket(func: uid(Ticket1)) @filter(uid(Ticket2)) { + x as updateTicket(func: uid(TicketRoot)) { uid } + TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) Ticket1 as var(func: type(Ticket)) @filter(uid(0x123)) - Ticket2 as var(func: uid(Ticket1)) @cascade { + TicketAuth2 as var(func: uid(Ticket1)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -407,10 +414,10 @@ Column5(func: uid(Column5)) { uid } - Column5.auth(func: uid(Column5)) @filter(uid(Column6)) { + Column5.auth(func: uid(Column5)) @filter(uid(ColumnAuth6)) { uid } - Column6 as var(func: uid(Column5)) @cascade { + ColumnAuth6 as var(func: uid(Column5)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -476,9 +483,11 @@ } dgquery: |- query { - x as updateLog(func: type(Log)) @filter(uid(0x123)) { + x as updateLog(func: uid(LogRoot)) { uid } + LogRoot as var(func: uid(Log1)) + Log1 as var(func: type(Log)) @filter(uid(0x123)) } - name: "Update with top level OR RBAC false." @@ -502,11 +511,12 @@ } dgquery: |- query { - x as updateProject(func: uid(Project1)) @filter(uid(Project2)) { + x as updateProject(func: uid(ProjectRoot)) { uid } + ProjectRoot as var(func: uid(Project1)) @filter(uid(ProjectAuth2)) Project1 as var(func: type(Project)) @filter(uid(0x123)) - Project2 as var(func: uid(Project1)) @cascade { + ProjectAuth2 as var(func: uid(Project1)) @cascade { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) dgraph.uid : uid @@ -536,9 +546,11 @@ } dgquery: |- query { - x as updateProject(func: type(Project)) @filter(uid(0x123)) { + x as updateProject(func: uid(ProjectRoot)) { uid } + ProjectRoot as var(func: uid(Project1)) + Project1 as var(func: type(Project)) @filter(uid(0x123)) } - name: "Update with top level And RBAC true." @@ -562,11 +574,12 @@ } dgquery: |- query { - x as updateIssue(func: uid(Issue1)) @filter(uid(Issue2)) { + x as updateIssue(func: uid(IssueRoot)) { uid } + IssueRoot as var(func: uid(Issue1)) @filter(uid(IssueAuth2)) Issue1 as var(func: type(Issue)) @filter(uid(0x123)) - Issue2 as var(func: uid(Issue1)) @cascade { + IssueAuth2 as var(func: uid(Issue1)) @cascade { owner : Issue.owner @filter(eq(User.username, "user1")) dgraph.uid : uid } @@ -617,9 +630,11 @@ } dgquery: |- query { - x as updateComplexLog(func: type(ComplexLog)) @filter(uid(0x123)) { + x as updateComplexLog(func: uid(ComplexLogRoot)) { uid } + ComplexLogRoot as var(func: uid(ComplexLog1)) + ComplexLog1 as var(func: type(ComplexLog)) @filter(uid(0x123)) } - name: "Update with top level not RBAC false." diff --git a/graphql/resolve/mutation.go b/graphql/resolve/mutation.go index 8eef0bb4ce1..e17927c220b 100644 --- a/graphql/resolve/mutation.go +++ b/graphql/resolve/mutation.go @@ -383,6 +383,7 @@ func authorizeNewNodes( authVariables: authVariables, varGen: NewVariableGenerator(), selector: addAuthSelector, + hasAuthRules: true, } // Collect all the newly created nodes in type groups @@ -416,8 +417,9 @@ func authorizeNewNodes( authQrys := make(map[string][]*gql.GraphQuery) for _, typeName := range createdTypes { typ := namesToType[typeName] - varName := newRw.varGen.Next(typ, "", "") + varName := newRw.varGen.Next(typ, "", "", false) newRw.varName = varName + newRw.parentVarName = typ.Name() + "Root" authQueries, authFilter := newRw.rewriteAuthQueries(typ) rn := newRw.selector(typ) diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index 560dea5a828..ec3fb48318d 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -103,7 +103,7 @@ func NewVariableGenerator() *VariableGenerator { // Next gets the Next variable name for the given type and xid. // So, if two objects of the same type have same value for xid field, // then they will get same variable name. -func (v *VariableGenerator) Next(typ schema.Type, xidName, xidVal string) string { +func (v *VariableGenerator) Next(typ schema.Type, xidName, xidVal string, auth bool) string { // return previously allocated variable name for repeating xidVal var key string if xidName == "" || xidVal == "" { @@ -118,7 +118,12 @@ func (v *VariableGenerator) Next(typ schema.Type, xidName, xidVal string) string // create new variable name v.counter++ - varName := fmt.Sprintf("%s%v", typ.Name(), v.counter) + var varName string + if auth { + varName = fmt.Sprintf("%sAuth%v", typ.Name(), v.counter) + } else { + varName = fmt.Sprintf("%s%v", typ.Name(), v.counter) + } // save it, if it was created for xidVal if xidName != "" && xidVal != "" { @@ -360,7 +365,10 @@ func (mrw *AddRewriter) FromMutationResult( authVariables: authVariables, varGen: NewVariableGenerator(), selector: queryAuthSelector, + parentVarName: mutation.MutatedType().Name() + "Root", } + authRw.hasAuthRules = hasAuthRules(mutation.QueryField(), authRw) + return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), errs } @@ -410,7 +418,9 @@ func (urw *UpdateRewriter) Rewrite( authVariables: authVariables, varGen: varGen, selector: updateAuthSelector, + parentVarName: m.MutatedType().Name() + "Root", } + authRw.hasAuthRules = hasAuthRules(m.QueryField(), authRw) upsertQuery := RewriteUpsertQueryFromMutation(m, authRw) srcUID := MutationQueryVarUID @@ -554,7 +564,9 @@ func (urw *UpdateRewriter) FromMutationResult( authVariables: authVariables, varGen: NewVariableGenerator(), selector: queryAuthSelector, + parentVarName: mutation.MutatedType().Name() + "Root", } + authRw.hasAuthRules = hasAuthRules(mutation.QueryField(), authRw) return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), nil } @@ -650,47 +662,11 @@ func RewriteUpsertQueryFromMutation(m schema.Mutation, authRw *authRewriter) *gq filter := extractFilter(m) addFilter(dgQuery, m.MutatedType(), filter) - if rbac == schema.Uncertain { - dgQuery = authRw.addAuthQueries(m.MutatedType(), dgQuery) - } + dgQuery = authRw.addAuthQueries(m.MutatedType(), dgQuery, rbac) return dgQuery } -// addUidFuncToQuery adds the uid func to the query with name `queryName`. This is useful when we -// have auth queries since the top level query might be present in the children. We pass -// `queryName` of top level query and it finds the appropriate query and adds `uidFunc` to it. -func addUidFuncToQuery(q *gql.GraphQuery, uidFunc *gql.Function, queryName string) { - // This handles the case when root auth query is a dummy query due to RBAC evaluation to false. - // In such case since the query doesn't return anything, we don't need to add uid func. - if q.Attr == queryName+"()" { - return - } - - if q.Attr != "" { - q.Func = uidFunc - return - } - - var query *gql.GraphQuery - for _, cq := range q.Children { - if cq.Attr == queryName { - query = cq - break - } - for _, ccq := range cq.Children { - if ccq.Attr == queryName { - query = ccq - break - } - } - } - - if query != nil { - query.Func = uidFunc - } -} - func (drw *deleteRewriter) Rewrite( ctx context.Context, m schema.Mutation) ([]*UpsertMutation, error) { @@ -712,7 +688,9 @@ func (drw *deleteRewriter) Rewrite( authVariables: authVariables, varGen: varGen, selector: deleteAuthSelector, + parentVarName: m.MutatedType().Name() + "Root", } + authRw.hasAuthRules = hasAuthRules(m.QueryField(), authRw) dgQry := RewriteUpsertQueryFromMutation(m, authRw) qry := dgQry @@ -735,7 +713,7 @@ func (drw *deleteRewriter) Rewrite( continue } } - varName := varGen.Next(fld.Type(), "", "") + varName := varGen.Next(fld.Type(), "", "", false) qry.Children = append(qry.Children, &gql.GraphQuery{ @@ -768,14 +746,15 @@ func (drw *deleteRewriter) Rewrite( authVariables: authVariables, varGen: varGen, selector: queryAuthSelector, + filterByUid: true, } + queryAuthRw.parentVarName = queryAuthRw.varGen.Next(queryField.Type(), "", "", + queryAuthRw.isWritingAuth) + queryAuthRw.varName = MutationQueryVar + queryAuthRw.hasAuthRules = hasAuthRules(queryField, authRw) + queryDel := rewriteAsQuery(queryField, queryAuthRw) - uidFunc := &gql.Function{ - Name: "uid", - Args: []gql.Arg{{Value: MutationQueryVar}}, - } - addUidFuncToQuery(queryDel, uidFunc, queryField.Name()) finalQry = &gql.GraphQuery{Children: append([]*gql.GraphQuery{dgQry}, queryDel)} } else { finalQry = dgQry @@ -937,7 +916,7 @@ func rewriteObject( atTopLevel := srcField == nil topLevelAdd := srcUID == "" - variable := varGen.Next(typ, "", "") + variable := varGen.Next(typ, "", "", false) id := typ.IDField() if id != nil { @@ -965,7 +944,7 @@ func rewriteObject( } // if the object has an xid, the variable name will be formed from the xidValue in order // to handle duplicate object addition/updation - variable = varGen.Next(typ, xid.Name(), xidString) + variable = varGen.Next(typ, xid.Name(), xidString, false) // check if an object with same xid has been encountered earlier if xidObj := xidMetadata.variableObjMap[variable]; xidObj != nil { // if we already encountered an object with same xid earlier, then we give error if: @@ -1454,7 +1433,7 @@ func addDelete( qryVar = qryVar[4 : len(qryVar)-1] } - targetVar := varGen.Next(qryFld.Type(), "", "") + targetVar := varGen.Next(qryFld.Type(), "", "", false) delFldName := qryFld.Type().DgraphPredicate(delFld.Name()) qry := &gql.GraphQuery{ @@ -1527,6 +1506,10 @@ func addDelete( varGen: varGen, varName: targetVar, selector: updateAuthSelector, + parentVarName: qryFld.Type().Name() + "Root", + } + if rn := newRw.selector(qryFld.Type()); rn != nil { + newRw.hasAuthRules = true } authQueries, authFilter := newRw.rewriteAuthQueries(qryFld.Type()) diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index 190f36a20c7..9658231f302 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -34,9 +34,18 @@ type queryRewriter struct{} type authRewriter struct { authVariables map[string]interface{} isWritingAuth bool - selector func(t schema.Type) *schema.RuleNode - varGen *VariableGenerator - varName string + // `filterByUid` is used to when we have to rewrite top level query with uid function. The + // variable name is passed in `varName`. If true it will rewrite as following: + // queryType(uid(varName)) { + // Once such case is when we perform query in delete mutation. + filterByUid bool + selector func(t schema.Type) *schema.RuleNode + varGen *VariableGenerator + varName string + // `parentVarName` is used to link a query with it's previous level. + parentVarName string + // `hasAuthRules` indicates if any of fields in the complete query hierarchy has auth rules. + hasAuthRules bool } // NewQueryRewriter returns a new QueryRewriter. @@ -44,11 +53,29 @@ func NewQueryRewriter() QueryRewriter { return &queryRewriter{} } +func hasAuthRules(field schema.Field, authRw *authRewriter) bool { + rn := authRw.selector(field.Type()) + if rn != nil { + return true + } + + for _, childField := range field.SelectionSet() { + if authRules := hasAuthRules(childField, authRw); authRules { + return true + } + } + return false +} + // Rewrite rewrites a GraphQL query into a Dgraph GraphQuery. func (qr *queryRewriter) Rewrite( ctx context.Context, gqlQuery schema.Query) (*gql.GraphQuery, error) { + if gqlQuery.Type().InterfaceImplHasAuthRules() { + return &gql.GraphQuery{Attr: gqlQuery.ResponseName() + "()"}, nil + } + authVariables, err := authorization.ExtractAuthVariables(ctx) if err != nil { return nil, err @@ -58,11 +85,9 @@ func (qr *queryRewriter) Rewrite( authVariables: authVariables, varGen: NewVariableGenerator(), selector: queryAuthSelector, + parentVarName: gqlQuery.Type().Name() + "Root", } - - if gqlQuery.Type().InterfaceImplHasAuthRules() { - return &gql.GraphQuery{Attr: gqlQuery.ResponseName() + "()"}, nil - } + authRw.hasAuthRules = hasAuthRules(gqlQuery, authRw) switch gqlQuery.QueryType() { case schema.GetQuery: @@ -220,9 +245,7 @@ func rewriteAsQueryByIds(field schema.Field, uids []uint64, authRw *authRewriter addUID(dgQuery) addCascadeDirective(dgQuery, field) - if rbac == schema.Uncertain { - dgQuery = authRw.addAuthQueries(field.Type(), dgQuery) - } + dgQuery = authRw.addAuthQueries(field.Type(), dgQuery, rbac) if len(selectionAuth) > 0 { dgQuery = &gql.GraphQuery{Children: append([]*gql.GraphQuery{dgQuery}, selectionAuth...)} @@ -318,9 +341,7 @@ func rewriteAsGet( addTypeFilter(dgQuery, field.Type()) addCascadeDirective(dgQuery, field) - if rbac == schema.Uncertain { - dgQuery = auth.addAuthQueries(field.Type(), dgQuery) - } + dgQuery = auth.addAuthQueries(field.Type(), dgQuery, rbac) if len(selectionAuth) > 0 { dgQuery = &gql.GraphQuery{Children: append([]*gql.GraphQuery{dgQuery}, selectionAuth...)} @@ -340,20 +361,20 @@ func rewriteAsQuery(field schema.Field, authRw *authRewriter) *gql.GraphQuery { return dgQuery } - if authRw != nil && authRw.isWritingAuth && authRw.varName != "" { + if authRw != nil && (authRw.isWritingAuth || authRw.filterByUid) && (authRw.varName != "" || authRw.parentVarName != "") { // When rewriting auth rules, they always start like // Todo2 as var(func: uid(Todo1)) @cascade { // Where Todo1 is the variable generated from the filter of the field // we are adding auth to. - // - // TODO: Currently this only applies at the top level. This means auth queries - // from the top level query/get are as efficient as the original query (because - // they start from the uid(Todo1) of the user query) ... however auth queries - // on deeper fields will start like `func: type(Todo)`, that's ok for building - // the feature and getting all the testing in place, but we should improve this so - // that the internal auth queries start from exactly the possible nodes that the - // internal field is considering. + authRw.addVariableUIDFunc(dgQuery) + // This is executed when querying while performing delete mutation request since + // in case of delete mutation we already have variable `MutationQueryVar` at root level. + if authRw.filterByUid { + // Since the variable is only added at the top level we reset the `authRW` variables. + authRw.varName = "" + authRw.filterByUid = false + } } else if ids := idFilter(field, field.Type().IDField()); ids != nil { addUIDFunc(dgQuery, ids) } else { @@ -365,9 +386,7 @@ func rewriteAsQuery(field schema.Field, authRw *authRewriter) *gql.GraphQuery { addUID(dgQuery) addCascadeDirective(dgQuery, field) - if rbac == schema.Uncertain { - dgQuery = authRw.addAuthQueries(field.Type(), dgQuery) - } + dgQuery = authRw.addAuthQueries(field.Type(), dgQuery, rbac) if len(selectionAuth) > 0 { dgQuery = &gql.GraphQuery{Children: append([]*gql.GraphQuery{dgQuery}, selectionAuth...)} @@ -387,7 +406,8 @@ func (authRw *authRewriter) writingAuth() bool { // original query and the auth. func (authRw *authRewriter) addAuthQueries( typ schema.Type, - dgQuery *gql.GraphQuery) *gql.GraphQuery { + dgQuery *gql.GraphQuery, + rbacEval schema.RuleResult) *gql.GraphQuery { // There's no need to recursively inject auth queries into other auth queries, so if // we are already generating an auth query, there's nothing to add. @@ -395,13 +415,18 @@ func (authRw *authRewriter) addAuthQueries( return dgQuery } - authRw.varName = authRw.varGen.Next(typ, "", "") + authRw.varName = authRw.varGen.Next(typ, "", "", authRw.isWritingAuth) fldAuthQueries, filter := authRw.rewriteAuthQueries(typ) - if len(fldAuthQueries) == 0 { + if len(fldAuthQueries) == 0 && !authRw.hasAuthRules { return dgQuery } + if rbacEval != schema.Uncertain { + fldAuthQueries = nil + filter = nil + } + // build a query like // Todo1 as var(func: ... ) @filter(...) // that has the filter from the user query in it. This is then used as @@ -415,14 +440,28 @@ func (authRw *authRewriter) addAuthQueries( Filter: dgQuery.Filter, } + rootQry := &gql.GraphQuery{ + Var: authRw.parentVarName, + Attr: "var", + Func: &gql.Function{ + Name: "uid", + Args: []gql.Arg{{Value: authRw.varName}}, + }, + Filter: filter, + Order: dgQuery.Order, + Args: dgQuery.Args, + } + + dgQuery.Filter = nil + dgQuery.Args = nil + // The user query starts from the var query generated above and is filtered // by the the filter generated from auth processing, so now we build // queryTodo(func: uid(Todo1)) @filter(...auth-queries...) { ... } dgQuery.Func = &gql.Function{ Name: "uid", - Args: []gql.Arg{{Value: authRw.varName}}, + Args: []gql.Arg{{Value: authRw.parentVarName}}, } - dgQuery.Filter = filter // The final query that includes the user's filter and auth processsing is thus like // @@ -430,13 +469,18 @@ func (authRw *authRewriter) addAuthQueries( // Todo1 as var(func: ... ) @filter(...) // Todo2 as var(func: uid(Todo1)) @cascade { ...auth query 1... } // Todo3 as var(func: uid(Todo1)) @cascade { ...auth query 2... } - return &gql.GraphQuery{Children: append([]*gql.GraphQuery{dgQuery, varQry}, fldAuthQueries...)} + return &gql.GraphQuery{Children: append([]*gql.GraphQuery{dgQuery, rootQry, varQry}, fldAuthQueries...)} } func (authRw *authRewriter) addVariableUIDFunc(q *gql.GraphQuery) { + varName := authRw.parentVarName + if authRw.varName != "" { + varName = authRw.varName + } + q.Func = &gql.Function{ Name: "uid", - Args: []gql.Arg{{Value: authRw.varName}}, + Args: []gql.Arg{{Value: varName}}, } } @@ -460,6 +504,8 @@ func (authRw *authRewriter) rewriteAuthQueries(typ schema.Type) ([]*gql.GraphQue isWritingAuth: true, varName: authRw.varName, selector: authRw.selector, + parentVarName: authRw.parentVarName, + hasAuthRules: authRw.hasAuthRules, }).rewriteRuleNode(typ, authRw.selector(typ)) } @@ -540,7 +586,7 @@ func (authRw *authRewriter) rewriteRuleNode( // build // Todo2 as var(func: uid(Todo1)) @cascade { ...auth query 1... } - varName := authRw.varGen.Next(typ, "", "") + varName := authRw.varGen.Next(typ, "", "", authRw.isWritingAuth) r1 := rewriteAsQuery(qry, authRw) r1.Var = varName r1.Attr = "var" @@ -589,7 +635,6 @@ func addTypeFunc(q *gql.GraphQuery, typ string) { Name: "type", Args: []gql.Arg{{Value: typ}}, } - } // addSelectionSetFrom adds all the selections from field into q, and returns a list @@ -621,7 +666,6 @@ func addSelectionSetFrom( Alias: "dgraph.uid", }) } - } // These fields might not have been requested by the user directly as part of the query but @@ -663,9 +707,25 @@ func addSelectionSetFrom( addCascadeDirective(child, f) rbac := auth.evaluateStaticRules(f.Type()) + // Since the recursion processes the query in bottom up way, we store the state of the so + // that we can restore it later. + var parentVarName, parentQryName string + if len(f.SelectionSet()) > 0 && !auth.isWritingAuth && auth.hasAuthRules { + parentVarName = auth.parentVarName + parentQryName = auth.varGen.Next(f.Type(), "", "", auth.isWritingAuth) + auth.parentVarName = parentQryName + auth.varName = parentQryName + } + selectionAuth := addSelectionSetFrom(child, f, auth) addedFields[f.Name()] = true + if len(f.SelectionSet()) > 0 && !auth.isWritingAuth && auth.hasAuthRules { + // Restore the state after processing is done. + auth.parentVarName = parentVarName + auth.varName = parentQryName + } + if rbac == schema.Positive || rbac == schema.Uncertain { q.Children = append(q.Children, child) } @@ -675,8 +735,6 @@ func addSelectionSetFrom( } fieldAuth, authFilter := auth.rewriteAuthQueries(f.Type()) - authQueries = append(authQueries, selectionAuth...) - authQueries = append(authQueries, fieldAuth...) if authFilter != nil { if child.Filter == nil { child.Filter = authFilter @@ -687,6 +745,52 @@ func addSelectionSetFrom( } } } + + if len(f.SelectionSet()) > 0 && !auth.isWritingAuth && auth.hasAuthRules { + // This adds the following query. + // var(func: uid(Ticket)) { + // User as Ticket.assignedTo + // } + // where `Ticket` is the nodes selected at parent level and `User` is the nodes we + // need on the current level. + parentQry := &gql.GraphQuery{ + Func: &gql.Function{ + Name: "uid", + Args: []gql.Arg{{Value: auth.parentVarName}}, + }, + Attr: "var", + Children: []*gql.GraphQuery{{Attr: f.DgraphPredicate(), Var: parentQryName}}, + } + + // This query aggregates all filters and auth rules and is used by root query to filter + // the final nodes for the current level. + // User6 as var(func: uid(User2), orderasc: ...) @filter((eq(User.username, "User1") AND (...Auth Filter)))) + filtervarName := auth.varGen.Next(f.Type(), "", "", auth.isWritingAuth) + selectionQry := &gql.GraphQuery{ + Var: filtervarName, + Attr: "var", + Func: &gql.Function{ + Name: "uid", + Args: []gql.Arg{{Value: parentQryName}}, + }, + } + + addArgumentsToField(selectionQry, f) + selectionQry.Filter = child.Filter + authQueries = append(authQueries, parentQry, selectionQry) + child.Filter = &gql.FilterTree{ + Func: &gql.Function{ + Name: "uid", + Args: []gql.Arg{{Value: filtervarName}}, + }, + } + + // We already apply the following to `selectionQry` by calling addArgumentsToField() + // hence they are no longer required. + child.Args = nil + } + authQueries = append(authQueries, selectionAuth...) + authQueries = append(authQueries, fieldAuth...) } // Sort the required fields before adding them to q.Children so that the query produced after