-
Notifications
You must be signed in to change notification settings - Fork 2k
[GitHub] Add a GraphQL client to the connector #3837
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
Changes from 4 commits
21d7c41
21398d7
fc0ac21
2912a20
f735824
5567a33
05a3781
b208544
efa6702
bac0091
bb7a44d
2355e48
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 |
---|---|---|
@@ -1,17 +1,92 @@ | ||
package github | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
|
||
gogit "github.com/go-git/go-git/v5" | ||
"github.com/google/go-github/v67/github" | ||
"github.com/shurcooL/githubv4" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/context" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/log" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" | ||
) | ||
|
||
const cloudEndpoint = "https://api.github.com" | ||
const ( | ||
cloudV3Endpoint = "https://api.github.com" | ||
cloudGraphqlEndpoint = "https://api.github.com/graphql" // https://docs.github.com/en/graphql/guides/forming-calls-with-graphql#the-graphql-endpoint | ||
) | ||
|
||
// Connector abstracts over the authenticated ways to interact with GitHub: cloning and API operations. | ||
type Connector interface { | ||
// APIClient returns a configured GitHub client that can be used for GitHub API operations. | ||
APIClient() *github.Client | ||
// GraphQLClient returns a client that can be used for GraphQL operations. | ||
GraphQLClient() *githubv4.Client | ||
// Clone clones a repository using the configured authentication information. | ||
Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) | ||
} | ||
|
||
func newConnector(ctx context.Context, source *Source) (Connector, error) { | ||
apiEndpoint := source.conn.Endpoint | ||
if apiEndpoint == "" || endsWithGithub.MatchString(apiEndpoint) { | ||
apiEndpoint = cloudV3Endpoint | ||
} | ||
|
||
switch cred := source.conn.GetCredential().(type) { | ||
case *sourcespb.GitHub_GithubApp: | ||
log.RedactGlobally(cred.GithubApp.GetPrivateKey()) | ||
return NewAppConnector(ctx, apiEndpoint, cred.GithubApp) | ||
case *sourcespb.GitHub_BasicAuth: | ||
log.RedactGlobally(cred.BasicAuth.GetPassword()) | ||
return NewBasicAuthConnector(ctx, apiEndpoint, source.conn.GetClonePath(), cred.BasicAuth) | ||
case *sourcespb.GitHub_Token: | ||
log.RedactGlobally(cred.Token) | ||
return NewTokenConnector(ctx, apiEndpoint, cred.Token, source.conn.GetClonePath(), source.useAuthInUrl, func(c context.Context, err error) bool { | ||
return source.handleRateLimit(c, err) | ||
}) | ||
case *sourcespb.GitHub_Unauthenticated: | ||
return NewUnauthenticatedConnector(ctx, apiEndpoint, source.conn.GetClonePath()) | ||
default: | ||
return nil, fmt.Errorf("unknown connection type %T", source.conn.GetCredential()) | ||
} | ||
} | ||
|
||
func createAPIClient(ctx context.Context, httpClient *http.Client, apiEndpoint string) (*github.Client, error) { | ||
ctx.Logger().V(2).Info("Creating API client", "url", apiEndpoint) | ||
|
||
// If we're using public GitHub, make a regular client. | ||
// Otherwise, make an enterprise client. | ||
if strings.EqualFold(apiEndpoint, cloudV3Endpoint) { | ||
return github.NewClient(httpClient), nil | ||
} | ||
|
||
return github.NewClient(httpClient).WithEnterpriseURLs(apiEndpoint, apiEndpoint) | ||
} | ||
|
||
func createGraphqlClient(ctx context.Context, client *http.Client, apiEndpoint string) (*githubv4.Client, error) { | ||
var graphqlEndpoint string | ||
if apiEndpoint == cloudV3Endpoint { | ||
graphqlEndpoint = cloudGraphqlEndpoint | ||
} else { | ||
// Use the root endpoint for the host. | ||
// https://docs.github.com/en/[email protected]/graphql/guides/introduction-to-graphql | ||
parsedURL, err := url.Parse(apiEndpoint) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create GraphQL client: %w", err) | ||
} | ||
|
||
// GitHub Enterprise uses `/api/v3` for the base. (https://github.com/google/go-github/issues/958) | ||
// Swap it, and anything before `/api`, with GraphQL. | ||
before, _ := strings.CutSuffix(parsedURL.Path, "/api/v3") | ||
parsedURL.Path = before + "/api/graphql" | ||
graphqlEndpoint = parsedURL.String() | ||
} | ||
|
||
ctx.Logger().V(2).Info("Creating GraphQL client", "url", graphqlEndpoint) | ||
|
||
return githubv4.NewEnterpriseClient(graphqlEndpoint, client), nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,39 +5,48 @@ import ( | |
|
||
gogit "github.com/go-git/go-git/v5" | ||
"github.com/google/go-github/v67/github" | ||
"github.com/shurcooL/githubv4" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/context" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" | ||
) | ||
|
||
type basicAuthConnector struct { | ||
apiClient *github.Client | ||
username string | ||
password string | ||
clonePath string | ||
apiClient *github.Client | ||
graphqlClient *githubv4.Client | ||
username string | ||
password string | ||
clonePath string | ||
} | ||
|
||
var _ Connector = (*basicAuthConnector)(nil) | ||
|
||
func NewBasicAuthConnector(apiEndpoint, clonePath string, cred *credentialspb.BasicAuth) (Connector, error) { | ||
func NewBasicAuthConnector(ctx context.Context, apiEndpoint, clonePath string, cred *credentialspb.BasicAuth) (Connector, error) { | ||
const httpTimeoutSeconds = 60 | ||
httpClient := common.RetryableHTTPClientTimeout(int64(httpTimeoutSeconds)) | ||
httpClient.Transport = &github.BasicAuthTransport{ | ||
Username: cred.Username, | ||
Password: cred.Password, | ||
} | ||
|
||
apiClient, err := createGitHubClient(httpClient, apiEndpoint) | ||
apiClient, err := createAPIClient(ctx, httpClient, apiEndpoint) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create API client: %w", err) | ||
} | ||
|
||
graphqlClient, err := createGraphqlClient(ctx, httpClient, apiEndpoint) | ||
if err != nil { | ||
return nil, err | ||
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. We wrap the API client creation error; we should either wrap this one as well, or wrap neither. |
||
} | ||
|
||
return &basicAuthConnector{ | ||
apiClient: apiClient, | ||
username: cred.Username, | ||
password: cred.Password, | ||
clonePath: clonePath, | ||
apiClient: apiClient, | ||
graphqlClient: graphqlClient, | ||
username: cred.Username, | ||
password: cred.Password, | ||
clonePath: clonePath, | ||
}, nil | ||
} | ||
|
||
|
@@ -48,3 +57,7 @@ func (c *basicAuthConnector) APIClient() *github.Client { | |
func (c *basicAuthConnector) Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) { | ||
return git.CloneRepoUsingToken(ctx, c.password, repoURL, c.clonePath, c.username, true, args...) | ||
} | ||
|
||
func (c *basicAuthConnector) GraphQLClient() *githubv4.Client { | ||
return c.graphqlClient | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,15 +7,19 @@ import ( | |
|
||
gogit "github.com/go-git/go-git/v5" | ||
"github.com/google/go-github/v67/github" | ||
"github.com/shurcooL/githubv4" | ||
"golang.org/x/oauth2" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/context" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
type tokenConnector struct { | ||
apiClient *github.Client | ||
token string | ||
token string | ||
apiClient *github.Client | ||
graphqlClient *githubv4.Client | ||
|
||
isGitHubEnterprise bool | ||
handleRateLimit func(context.Context, error) bool | ||
user string | ||
|
@@ -26,7 +30,7 @@ type tokenConnector struct { | |
|
||
var _ Connector = (*tokenConnector)(nil) | ||
|
||
func NewTokenConnector(apiEndpoint, token, clonePath string, authInUrl bool, handleRateLimit func(context.Context, error) bool) (Connector, error) { | ||
func NewTokenConnector(ctx context.Context, apiEndpoint, token, clonePath string, authInUrl bool, handleRateLimit func(context.Context, error) bool) (Connector, error) { | ||
const httpTimeoutSeconds = 60 | ||
httpClient := common.RetryableHTTPClientTimeout(int64(httpTimeoutSeconds)) | ||
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) | ||
|
@@ -35,15 +39,21 @@ func NewTokenConnector(apiEndpoint, token, clonePath string, authInUrl bool, han | |
Source: tokenSource, | ||
} | ||
|
||
apiClient, err := createGitHubClient(httpClient, apiEndpoint) | ||
apiClient, err := createAPIClient(ctx, httpClient, apiEndpoint) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create API client: %w", err) | ||
} | ||
|
||
graphqlClient, err := createGraphqlClient(ctx, httpClient, apiEndpoint) | ||
if err != nil { | ||
return nil, err | ||
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. same error-wrapping comment as above |
||
} | ||
|
||
return &tokenConnector{ | ||
apiClient: apiClient, | ||
graphqlClient: graphqlClient, | ||
token: token, | ||
isGitHubEnterprise: !strings.EqualFold(apiEndpoint, cloudEndpoint), | ||
isGitHubEnterprise: !strings.EqualFold(apiEndpoint, cloudV3Endpoint), | ||
handleRateLimit: handleRateLimit, | ||
authInUrl: authInUrl, | ||
clonePath: clonePath, | ||
|
@@ -62,6 +72,10 @@ func (c *tokenConnector) Clone(ctx context.Context, repoURL string, args ...stri | |
return git.CloneRepoUsingToken(ctx, c.token, repoURL, c.clonePath, c.user, c.authInUrl, args...) | ||
} | ||
|
||
func (c *tokenConnector) GraphQLClient() *githubv4.Client { | ||
return c.graphqlClient | ||
} | ||
|
||
func (c *tokenConnector) IsGithubEnterprise() bool { | ||
return c.isGitHubEnterprise | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,29 +5,39 @@ import ( | |
|
||
gogit "github.com/go-git/go-git/v5" | ||
"github.com/google/go-github/v67/github" | ||
"github.com/shurcooL/githubv4" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/context" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/git" | ||
) | ||
|
||
type unauthenticatedConnector struct { | ||
apiClient *github.Client | ||
apiClient *github.Client | ||
graphqlClient *githubv4.Client | ||
|
||
clonePath string | ||
} | ||
|
||
var _ Connector = (*unauthenticatedConnector)(nil) | ||
|
||
func NewUnauthenticatedConnector(apiEndpoint, clonePath string) (Connector, error) { | ||
func NewUnauthenticatedConnector(ctx context.Context, apiEndpoint, clonePath string) (Connector, error) { | ||
const httpTimeoutSeconds = 60 | ||
httpClient := common.RetryableHTTPClientTimeout(int64(httpTimeoutSeconds)) | ||
apiClient, err := createGitHubClient(httpClient, apiEndpoint) | ||
apiClient, err := createAPIClient(ctx, httpClient, apiEndpoint) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create API client: %w", err) | ||
} | ||
|
||
graphqlClient, err := createGraphqlClient(ctx, httpClient, apiEndpoint) | ||
if err != nil { | ||
return nil, err | ||
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. same error-wrapping comment as above |
||
} | ||
|
||
return &unauthenticatedConnector{ | ||
apiClient: apiClient, | ||
clonePath: clonePath, | ||
apiClient: apiClient, | ||
graphqlClient: graphqlClient, | ||
clonePath: clonePath, | ||
}, nil | ||
} | ||
|
||
|
@@ -38,3 +48,7 @@ func (c *unauthenticatedConnector) APIClient() *github.Client { | |
func (c *unauthenticatedConnector) Clone(ctx context.Context, repoURL string, args ...string) (string, *gogit.Repository, error) { | ||
return git.CloneRepoUsingUnauthenticated(ctx, repoURL, c.clonePath, args...) | ||
} | ||
|
||
func (c *unauthenticatedConnector) GraphQLClient() *githubv4.Client { | ||
return c.graphqlClient | ||
} |
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.
I can't decide if this is very nitpicky or a strong commitment to consistency, buuuuut I think we could improve this little thing by doing:
...and then anything calling
createGraphqlClient
can then wrap that with "error creating GraphQL client: %w". It is definitely double wrapping, but for someone looking into what might have gone wrong, it's more explicit about what happened and that's super helpful.Option 2 IMO is, it seems like most of what this function does is validate a REST API url and convert it to a GraphQL endpoint URL. That could be its own function, which would be very testable, and then an "error parsing URL" error makes even more sense. Or, it might not need its own function--there's a very natural spot for it up in
newConnector
(line 38-ish), and then you can pass a known-validgraphqlEndpoint
URL to the different connector constructors, which then don't have to check for errors when creating their GraphQL clients as it (apparently) can't error.I appreciate that's more work though? I think just the little fix from option 1 is 👌🏻