package graphqlhelpers import ( "encoding/json" "fmt" "strings" "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/parser" ) const ( SensitiveMask = "******************" ) var ( sensitiveParams = map[string]bool{ "password": true, "key": true, "secret": true, "token": true, "oktaToken": true, "refreshToken": true, "newPassword": true, "secretValue": true, } ) // Query type Query struct { Name string SelectedFields []*SelectedField } // SelectedField type SelectedField struct { Name string SubFields []*SelectedField } // GetOperation returns the graphql operation mutation/query/subscription. func GetOperation(rctx *graphql.OperationContext) *ast.Operation { if rctx != nil && rctx.Operation != nil { return &rctx.Operation.Operation } return nil } // GetRawQuery returns the graphql query. func GetRawQuery(rctx *graphql.OperationContext) string { if rctx != nil { return rctx.RawQuery } return "" } // GetVariables func GetVariables(rctx *graphql.OperationContext) map[string]interface{} { vars := make(map[string]interface{}) if rctx != nil { for k, v := range rctx.Variables { vars[strings.ToUpper(k)] = v } } return vars } // ParseQuery parses the graphql query into a QueryDocument. func ParseQuery(query string) (*ast.QueryDocument, error) { src := &ast.Source{ Input: query, } schema, err := parser.ParseQuery(src) if err != nil { return nil, gqlerror.List{&gqlerror.Error{ Err: err, }} } return schema, nil } // TODO(pa250194_ncrvoyix): this function is supposed to ident and stringify the GraphQL queries. func (q Query) String() string { // ident the query sufficiently res, _ := json.Marshal(q) return string(res) } // GetOperations func GetOperations(schema *ast.QueryDocument) string { if schema == nil { return "" } var operations strings.Builder for idx, re := range schema.Operations { if idx == 0 { operations.WriteString(string(re.Operation)) } else { operations.WriteString(fmt.Sprintf(", %s", re.Operation)) } } return operations.String() } // GetQueries func GetQueries(schema *ast.QueryDocument) []*Query { queries := make([]*Query, 0) if schema == nil { return queries } for _, op := range schema.Operations { for _, selection := range op.SelectionSet { field := selection.(*ast.Field) query := &Query{ Name: field.Name, SelectedFields: make([]*SelectedField, 0), } query.SelectedFields = recursiveField(field) queries = append(queries, query) } } return queries } // SanitizeDocument func SanitizeDocument(schema *ast.QueryDocument) { if schema == nil { return } for _, re := range schema.Operations { for _, selection := range re.SelectionSet { if field, ok := selection.(*ast.Field); ok { for _, args := range field.Arguments { if _, exists := sensitiveParams[args.Name]; exists { args.Value = &ast.Value{ Raw: SensitiveMask, Kind: ast.StringValue, } } } } } } } // recursiveField recusively fetches the selected fields of a specific field // Example Query: // // users { // name // contact { // phone // email // } // } // // Returns: [{Name: "name", SubFields: null}, {Name: "contact", SubFields: [{Name: "phone", SubFields: null}, {Name: "email", SubFields: null}]}] func recursiveField(field *ast.Field) []*SelectedField { out := make([]*SelectedField, 0) for _, fss := range field.SelectionSet { res := fss.(*ast.Field) sf := &SelectedField{ Name: res.Name, } recursiveSelectionSet(res.SelectionSet, sf) out = append(out, sf) } return out } // recursiveSelectionSet func recursiveSelectionSet(ss ast.SelectionSet, sf *SelectedField) { for _, fss := range ss { res := fss.(*ast.Field) field := SelectedField{ Name: res.Name, } if len(res.SelectionSet) > 0 { field.SubFields = make([]*SelectedField, 0) for _, fss := range res.SelectionSet { res := fss.(*ast.Field) field.SubFields = append(field.SubFields, &SelectedField{ Name: res.Name, }) sf.SubFields = append(sf.SubFields, &field) recursiveSelectionSet(res.SelectionSet, sf) } } else { sf.SubFields = append(sf.SubFields, &field) } } } // GetQueryNames returns the graphql query names example: WhoAmI, Login, Logout etc. func GetQueryNames(schema *ast.QueryDocument) []string { names := make([]string, 0) if schema == nil { return names } for _, re := range schema.Operations { for _, selection := range re.SelectionSet { field := selection.(*ast.Field) names = append(names, field.Name) } } return names } // GetParams returns the graphql query parameters and values. func GetParams(opctx *graphql.OperationContext, schema *ast.QueryDocument) map[string]interface{} { params := make(map[string]interface{}, 0) if schema == nil { return params } for _, re := range schema.Operations { for _, selection := range re.SelectionSet { if field, ok := selection.(*ast.Field); ok { for _, args := range field.Arguments { params[args.Name] = args.Value.String() } } } } if opctx != nil { vars := GetVariables(opctx) for key, value := range params { v := value if val, exists := vars[strings.ToUpper(key)]; exists { v = val } if exists := sensitiveParams[key]; exists { v = SensitiveMask } params[key] = v } } return params } // GetResponseStatus returns the graphql query status. // Partial Failure if partial data and error(s) were returned. // Failure if error(s) were returned with no data. // Success if no error(s) were returned but data was returned. func GetResponseStatus(resp *graphql.Response) string { const nullStr = "null" switch { case resp != nil && len(resp.Errors) > 0 && string(resp.Data) != nullStr: return "Partial Failure" case resp != nil && len(resp.Errors) > 0 && string(resp.Data) == nullStr: return "Failure" case resp != nil && len(resp.Errors) == 0 && string(resp.Data) != nullStr: return "Success" default: return "Unknown" } } // UpdateQueryWithVariables updates the graphql document with variables. these variables values are inlined in the query // Example: // Query: mutation login($username: String!, $password: String!, $organization: String!) {\n login(username: $username, password: $password, organization: $organization) {\n fullName\n firstName\n credentialsExpired\n token\n __typename\n }\n}\n // Becomes: mutation login($username: String!, $password: String!, $organization: String!) {\n login(username: \"test-user\", password: \"123456\", organization: \"test-org\") {\n fullName\n firstName\n credentialsExpired\n token\n __typename\n }\n}\n func UpdateQueryWithVariables(doc *ast.QueryDocument, variables map[string]interface{}) { if doc == nil { return } for _, op := range doc.Operations { for idx, selection := range op.SelectionSet { field, ok := selection.(*ast.Field) if ok { for i, arg := range field.Arguments { argName := strings.Trim(arg.Value.String(), "$") val, exists := variables[strings.ToUpper(argName)] if !exists && argName == "" { // if the parameter is not provided but is in the query // set that param to null arg.Value.Raw = "" arg.Value.Kind = ast.NullValue continue } kind := getAstKind(val) arg.Value.Kind = kind getInnerVal(kind, arg.Value, val, (*[]*ast.ChildValue)(&arg.Value.Children)) field.Arguments[i] = arg } } op.SelectionSet[idx] = field } } } // getInnerVal is a helper function to set the field argument kind and raw value // if the type is object or array, we recursively loop through to get the sub value if any. func getInnerVal(kind ast.ValueKind, arg *ast.Value, val interface{}, children *[]*ast.ChildValue) { switch kind { case ast.StringValue, ast.BlockValue, ast.EnumValue, ast.Variable: arg.Raw = fmt.Sprintf("%s", val) arg.Kind = ast.StringValue case ast.IntValue: arg.Raw = fmt.Sprintf("%v", val) arg.Kind = ast.IntValue case ast.FloatValue: arg.Raw = fmt.Sprintf("%v", val) arg.Kind = ast.FloatValue case ast.BooleanValue: arg.Raw = fmt.Sprintf("%v", val) arg.Kind = ast.BooleanValue case ast.NullValue: arg.Kind = ast.NullValue case ast.ObjectValue: elem, ok := val.(map[string]interface{}) if ok { for key, value := range elem { newChild := &ast.ChildValue{ Name: key, Value: &ast.Value{ Kind: ast.ObjectValue, }, } childKind := getAstKind(value) arg.Children = append(arg.Children, newChild) getInnerVal(childKind, newChild.Value, value, children) } } else { arg.Raw = fmt.Sprintf("%v", val) arg.Kind = ast.StringValue } case ast.ListValue: switch val := val.(type) { case []string: getSubVal(val, children) case []int: getSubVal(val, children) case []int8: getSubVal(val, children) case []int16: getSubVal(val, children) case []int32: getSubVal(val, children) case []int64: getSubVal(val, children) case []float32: getSubVal(val, children) case []float64: getSubVal(val, children) case []bool: getSubVal(val, children) case []any: getSubVal(val, children) } } } func getSubVal[T any](arr []T, children *[]*ast.ChildValue) { for _, elem := range arr { childKind := getAstKind(elem) newChild := &ast.ChildValue{Value: &ast.Value{Kind: childKind}} *children = append(*children, newChild) getInnerVal(childKind, newChild.Value, elem, (*[]*ast.ChildValue)(&newChild.Value.Children)) } } func getAstKind(_var interface{}) ast.ValueKind { switch _var.(type) { case int, int8, int16, int32, int64: return ast.IntValue case float32, float64: return ast.FloatValue case string: return ast.StringValue case bool: return ast.BooleanValue case nil: return ast.NullValue case []int, []int8, []int16, []int32, []int64, []string, []bool, []any: return ast.ListValue case struct{}, interface{}: return ast.ObjectValue default: return ast.StringValue } } func IsMutation(rctx *graphql.OperationContext) bool { return rctx != nil && rctx.Operation != nil && rctx.Operation.Operation == ast.Mutation }