-
Notifications
You must be signed in to change notification settings - Fork 4
chore: Add graphql to the config for Github #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,8 @@ import ( | |
"reflect" | ||
"strings" | ||
|
||
"github.com/graphql-go/graphql/language/ast" | ||
"github.com/graphql-go/graphql/language/parser" | ||
log "github.com/sirupsen/logrus" | ||
|
||
"github.com/mcuadros/go-defaults" | ||
|
@@ -178,6 +180,75 @@ func httpMethodsDecodeHook(f reflect.Type, t reflect.Type, data interface{}) (in | |
return ParseHttpMethods(methods), nil | ||
} | ||
|
||
type graphQlRequest struct { | ||
Query string `json:"query"` | ||
OperationName string `json:"operationName,omitempty"` | ||
Variables map[string]interface{} `json:"variables,omitempty"` | ||
} | ||
|
||
func (config *InboundProxyConfig) validateGraphQLRequest(body []byte, filter *GraphQLFilter) error { | ||
if filter == nil { | ||
return nil | ||
} | ||
|
||
var req graphQlRequest | ||
if err := json.Unmarshal(body, &req); err != nil { | ||
return fmt.Errorf("invalid GitHub GraphQL request JSON: %v", err) | ||
} | ||
|
||
doc, err := parser.Parse(parser.ParseParams{ | ||
Source: req.Query, | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("graphql query is unparseable: %v", err) | ||
} | ||
|
||
// GitHub GraphQL requests typically have a single operation | ||
var foundOperation *ast.OperationDefinition | ||
for _, def := range doc.Definitions { | ||
if opDef, ok := def.(*ast.OperationDefinition); ok { | ||
foundOperation = opDef | ||
break | ||
} | ||
} | ||
|
||
if foundOperation == nil { | ||
return fmt.Errorf("no GraphQL operation found") | ||
} | ||
|
||
opType := string(foundOperation.Operation) | ||
opName := "" | ||
if foundOperation.Name != nil { | ||
opName = foundOperation.Name.Value | ||
} | ||
|
||
// If operation name is provided in the request, it must match the query | ||
if req.OperationName != "" && opName != req.OperationName { | ||
return fmt.Errorf("operation name mismatch between request and query") | ||
} | ||
|
||
// Validate against allowed operations | ||
allowedOps, exists := filter.AllowedOperations[opType] | ||
if !exists { | ||
return fmt.Errorf("GitHub GraphQL operation type '%s' not allowed", opType) | ||
} | ||
|
||
if opName == "" { | ||
return fmt.Errorf("GitHub GraphQL operations must be named") | ||
} | ||
|
||
for _, allowedOp := range allowedOps { | ||
if allowedOp == opName { | ||
return nil // Operation is allowed | ||
} | ||
} | ||
|
||
return fmt.Errorf("GitHub GraphQL %s operation '%s' not allowed", opType, opName) | ||
} | ||
|
||
type GraphQLFilter struct { | ||
AllowedOperations map[string][]string `validate:"required"` // map[operationType][]operationName | ||
} | ||
type AllowlistItem struct { | ||
URL string `mapstructure:"url" json:"url"` | ||
Methods HttpMethods `mapstructure:"methods" json:"methods"` | ||
|
@@ -187,6 +258,7 @@ type AllowlistItem struct { | |
LogRequestHeaders bool `mapstructure:"logRequestHeaders" json:"logRequestHeaders"` | ||
LogResponseBody bool `mapstructure:"logResponseBody" json:"logResponseBody"` | ||
LogResponseHeaders bool `mapstructure:"logResponseHeaders" json:"logResponseHeaders"` | ||
GraphQLData *GraphQLFilter `mapstructure:"githubGraphQL" json:"githubGraphQL"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you update the mapstructure and json tags too please? |
||
} | ||
|
||
type Allowlist []AllowlistItem | ||
|
@@ -420,6 +492,10 @@ func LoadConfig(configFiles []string, deploymentId int) (*Config, error) { | |
if err != nil { | ||
return nil, fmt.Errorf("failed to parse github base URL: %v", err) | ||
} | ||
gitHubBaseUrlGraphQL, err := url.Parse(strings.Replace(gitHub.BaseURL, "/api/v3", "/api/graphql", 1)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this feels a bit brittle imo. we've already parsed |
||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse github GraphQL base URL: %v", err) | ||
} | ||
|
||
var headers map[string]string | ||
if gitHub.Token != "" { | ||
|
@@ -517,6 +593,23 @@ func LoadConfig(configFiles []string, deploymentId int) (*Config, error) { | |
Methods: ParseHttpMethods([]string{"GET", "PUT"}), | ||
SetRequestHeaders: headers, | ||
}, | ||
// Graphql API with specific operations | ||
AllowlistItem{ | ||
URL: gitHubBaseUrlGraphQL.String(), | ||
Methods: ParseHttpMethods([]string{"POST"}), | ||
SetRequestHeaders: headers, | ||
GraphQLData: &GraphQLFilter{ | ||
AllowedOperations: map[string][]string{ | ||
"query": { | ||
"GetBlameDetails", | ||
}, | ||
"mutation": { | ||
"resolveReviewThread", | ||
"unresolveReviewThread", | ||
}, | ||
}, | ||
}, | ||
}, | ||
) | ||
|
||
if config.Inbound.GitHub.AllowCodeAccess { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,6 +79,28 @@ func (config *InboundProxyConfig) Start(tnet *netstack.Net) error { | |
return | ||
} | ||
|
||
// Just to make sure validate all three of these things before checking | ||
if allowlistMatch.GraphQLData != nil && | ||
c.Request.Method == "POST" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will there ever be graphql PUTs or PATCHes? might be safer to do |
||
|
||
bodyBytes, err := io.ReadAll(c.Request.Body) | ||
if err != nil { | ||
logger.WithError(err).Warn("github_graphql.read_body_error") | ||
c.Header(errorResponseHeader, "1") | ||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"}) | ||
return | ||
} | ||
// Restore the body for later use | ||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | ||
|
||
if err := config.validateGraphQLRequest(bodyBytes, allowlistMatch.GraphQLData); err != nil { | ||
logger.WithError(err).Warn("github_graphql.validation_error") | ||
c.Header(errorResponseHeader, "1") | ||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) | ||
return | ||
} | ||
} | ||
|
||
logger = logger.WithField("allowlist_match", allowlistMatch.URL) | ||
|
||
instrumentedTransport, err := BuildInstrumentedRoundTripper(transport, allowlistMatch.URL) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uber nit but let's have this be public like everything else (i.e. capital G)