169 changes: 150 additions & 19 deletions graphql/schema/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
package schema

import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"strings"

"github.com/spf13/cast"

"github.com/dgraph-io/dgraph/gql"

"github.com/dgraph-io/gqlparser/v2/ast"
Expand All @@ -35,7 +40,8 @@ const (
type RBACQuery struct {
Variable string
Operator string
Operand string
Operand interface{}
regex *regexp.Regexp
}

type RuleNode struct {
Expand Down Expand Up @@ -64,15 +70,65 @@ const (
Negative
)

func (rq *RBACQuery) EvaluateRBACRule(av map[string]interface{}) RuleResult {
if rq.Operator == "eq" {
if av[rq.Variable] == rq.Operand {
func (rq *RBACQuery) checkIfMatchInArray(array []interface{}) RuleResult {
for _, v := range array {
if rq.checkIfMatch(v) == Positive {
return Positive
}
}
return Negative
}

func (rq *RBACQuery) checkIfMatch(value interface{}) RuleResult {
rules, ok := rq.Operand.([]interface{})
if ok {
// this means rule operand is array slice
for _, r := range rules {
if evaluate(r, value, rq.regex) == Positive {
return Positive
}
}
return Negative
}
return evaluate(rq.Operand, value, rq.regex)
}

func evaluate(operand interface{}, value interface{}, regex *regexp.Regexp) RuleResult {
if regex != nil {
sval, ok := value.(string)
if ok && regex.MatchString(sval) {
return Positive
}
return Negative
}

if reflect.DeepEqual(value, operand) {
return Positive
}

return Negative
}

// EvaluateRBACRule evaluates the auth token based on the auth query
// There are two cases here:
// 1. Auth token has an array of values for the variable.
// 2. Auth token has non-array value for the variable.
// match would be deep equal except for regex match in case of regexp operator.
// In case array one match would made the rule positive.
// For example, Rule {$USER: { eq:"uid"}} and token $USER:["u", "id", "uid"] result in match.
// Rule {$USER: { in: ["uid", "xid"]}} and token $USER:["u", "id", "uid"] result in match
func (rq *RBACQuery) EvaluateRBACRule(av map[string]interface{}) RuleResult {
tokenValues, tokenCastErr := cast.ToSliceE(av[rq.Variable])
// if eq, auth rule value will be matched completely
// if regexp, auth rule value should always be string and so as token values
// if in, auth rule will only have array as the value check has to consider that
if tokenCastErr != nil {
// this means value for variable in token in not an array
return rq.checkIfMatch(av[rq.Variable])
}
return rq.checkIfMatchInArray(tokenValues)
}

func (node *RuleNode) staticEvaluation(av map[string]interface{}) RuleResult {
for _, v := range node.Variables {
if _, ok := av[v.Variable]; !ok {
Expand Down Expand Up @@ -188,7 +244,11 @@ func authRules(s *ast.Schema) (map[string]*TypeAuth, error) {
for _, intrface := range typ.Interfaces {
interfaceName := typeName(s.Types[intrface])
if authRules[interfaceName] != nil && authRules[interfaceName].Rules != nil {
authRules[name].Rules = mergeAuthRules(authRules[name].Rules, authRules[interfaceName].Rules, mergeAuthNodeWithAnd)
authRules[name].Rules = mergeAuthRules(
authRules[name].Rules,
authRules[interfaceName].Rules,
mergeAuthNodeWithAnd,
)
}
}
}
Expand Down Expand Up @@ -235,7 +295,11 @@ func mergeAuthNodeWithAnd(objectAuth, interfaceAuth *RuleNode) *RuleNode {
return ruleNode
}

func mergeAuthRules(objectAuthRules, interfaceAuthRules *AuthContainer, mergeAuthNode func(*RuleNode, *RuleNode) *RuleNode) *AuthContainer {
func mergeAuthRules(
objectAuthRules,
interfaceAuthRules *AuthContainer,
mergeAuthNode func(*RuleNode, *RuleNode) *RuleNode,
) *AuthContainer {
// return copy of interfaceAuthRules since it is a pointer and otherwise it will lead
// to unnecessary errors
if objectAuthRules == nil {
Expand Down Expand Up @@ -345,7 +409,7 @@ func parseAuthNode(s *ast.Schema, typ *ast.Definition, val *ast.Value) (*RuleNod
if rule := val.Children.ForName("rule"); rule != nil {
var err error
if strings.HasPrefix(rule.Raw, RBACQueryPrefix) {
result.RBACRule, err = rbacValidateRule(typ, rule.Raw)
result.RBACRule, err = getRBACQuery(typ, rule.Raw)
} else {
err = gqlValidateRule(s, typ, rule.Raw, result)
}
Expand All @@ -361,9 +425,9 @@ func parseAuthNode(s *ast.Schema, typ *ast.Definition, val *ast.Value) (*RuleNod
return result, errResult
}

func rbacValidateRule(typ *ast.Definition, rule string) (*RBACQuery, error) {
func getRBACQuery(typ *ast.Definition, rule string) (*RBACQuery, error) {
rbacRegex, err :=
regexp.Compile(`^{[\s]?(.*?)[\s]?:[\s]?{[\s]?(\w*)[\s]?:[\s]?"(.*)"[\s]?}[\s]?}$`)
regexp.Compile(`^{[\s]?(.*?)[\s]?:[\s]?{[\s]?(\w*)[\s]?:[\s]?(.*)[\s]?}[\s]?}$`)
if err != nil {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` error while parsing rule.",
typ.Name, err)
Expand All @@ -374,24 +438,91 @@ func rbacValidateRule(typ *ast.Definition, rule string) (*RBACQuery, error) {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` is not a valid rule.",
typ.Name, rule)
}
// bool, for booleans
// float64, for numbers
// string, for strings
// []interface{}, for JSON arrays
// map[string]interface{}, for JSON objects
// nil for JSON null
var op interface{}
if err = json.Unmarshal([]byte(rule[idx[0][6]:idx[0][7]]), &op); err != nil {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` is not a valid GraphQL variable.",
typ.Name, rule[idx[0][2]:idx[0][3]])
}

query := RBACQuery{
//objects with nil values are not supported in rules
if op == nil {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` operator has invalid value. "+
"null values aren't supported.", typ.Name, rule[idx[0][4]:idx[0][5]])
}
query := &RBACQuery{
Variable: rule[idx[0][2]:idx[0][3]],
Operator: rule[idx[0][4]:idx[0][5]],
Operand: rule[idx[0][6]:idx[0][7]],
Operand: op,
}

if !strings.HasPrefix(query.Variable, "$") {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` is not a valid GraphQL variable.",
typ.Name, query.Variable)
if err = validateRBACQuery(typ, query); err != nil {
return nil, err
}
// we have validated that variable is like $XYZ.
// For further uses we will ensure that we won't get the $ sign while evaluation
query.Variable = query.Variable[1:]

if query.Operator != "eq" {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` operator is not supported in "+
"this rule.", typ.Name, query.Operator)
// we will be sticking to compile once principle.
// regex in rule will be compiled once and used again.
if query.Operator == "regexp" {
query.regex, err = regexp.Compile(query.Operand.(string))
if err != nil {
return nil, gqlerror.Errorf("Type %s: @auth: `%s` does not have a valid regex expression.",
typ.Name, query.Variable)
}
}
return query, nil
}

func validateRBACQuery(typ *ast.Definition, rbacQuery *RBACQuery) error {
// validate rule operators
if ok, reason := validateRBACOperators(typ, rbacQuery); !ok {
return gqlerror.Errorf(reason)
}
return &query, nil

// validate variable name
if !strings.HasPrefix(rbacQuery.Variable, "$") {
return gqlerror.Errorf("Type %s: @auth: `%s` is not a valid GraphQL variable.",
typ.Name, rbacQuery.Variable)
}
return nil
}

func validateRBACOperators(typ *ast.Definition, query *RBACQuery) (bool, string) {
switch query.Operator {
case "eq":
// Array values in eq operator will not be supported.
// They are handled in a different way to manage all possible situations
_, isArray := query.Operand.([]interface{})
if isArray {
return false, fmt.Sprintf("Type %s: @auth: `%s` operator has invalid value `%v`."+
" Array values in eq operator will not be supported.",
typ.Name, query.Operator, query.Operand)
}
case "regexp":
_, ok := query.Operand.(string)
if !ok {
return false, fmt.Sprintf("Type %s: @auth: `%s` operator has invalid value `%v`."+
" Value should be of type String.", typ.Name, query.Operator, query.Operand)
}
case "in":
// auth rule value should be of array type
_, ok := query.Operand.([]interface{})
if !ok {
return false, fmt.Sprintf("Type %s: @auth: `%s` operator has invalid value `%v`."+
" Value should be an array.", typ.Name, query.Operator, query.Operand)
}
default:
return false, fmt.Sprintf("Type %s: @auth: `%s` operator is not supported.",
typ.Name, query.Operator)
}

return true, ""
}

func gqlValidateRule(s *ast.Schema, typ *ast.Definition, rule string, node *RuleNode) error {
Expand Down
65 changes: 64 additions & 1 deletion graphql/schema/auth_schemas_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,69 @@ invalid_schemas:
Did you mean userRole or username?]"}
]

- name: "Invalid RBAC rule: in filter not array variable 1"
input: |
type X @auth(
query: { rule: "{$USER: { in: \"xyz@dgraph.io\" } }"}
) {
username: String! @id
userRole: String @search(by: [hash])
}
errlist: [
{ "message": "Type X: @auth: `in` operator has invalid value `xyz@dgraph.io`.
Value should be an array." }
]

- name: "Invalid RBAC rule: in filter not array variable 2"
input: |
type X @auth(
query: { rule: "{$USER: { in: true } }"}
) {
username: String! @id
userRole: String @search(by: [hash])
}
errlist: [
{ "message": "Type X: @auth: `in` operator has invalid value `true`.
Value should be an array."}
]

- name: "Invalid RBAC rule: nil as the value"
input: |
type X @auth(
query: { rule: "{$USER: { eq: nil } }"}
) {
username: String! @id
userRole: String @search(by: [hash])
}
errlist: [
{ "message": "Type X: @auth: `$USER` is not a valid GraphQL variable." }
]

- name: "Invalid RBAC rule: null as the value"
input: |
type X @auth(
query: { rule: "{$USER: { eq: null } }"}
) {
username: String! @id
userRole: String @search(by: [hash])
}
errlist: [
{ "message": "Type X: @auth: `eq` operator has invalid value. null values aren't supported." }
]

- name: "Invalid RBAC rule: regexp filter not string variable"
input: |
type X @auth(
query: { rule: "{$USER: { regexp: 12345 } }"}
) {
username: String! @id
userRole: String @search(by: [hash])
}
errlist: [
{ "message": "Type X: @auth: `regexp` operator has invalid value `12345`.
Value should be of type String." }
]

- name: "RBAC rule invalid variable"
input: |
type X @auth(
Expand All @@ -47,7 +110,7 @@ invalid_schemas:
username: String! @id
userRole: String @search(by: [hash])
}
errlist: [{"message": "Type X: @auth: `xyz` operator is not supported in this rule."}]
errlist: [{"message": "Type X: @auth: `xyz` operator is not supported."}]

- name: "Invalid RBAC rule"
input: |
Expand Down