diff --git a/enterprise/dev/ci/scripts/app-token/main.go b/enterprise/dev/ci/scripts/app-token/main.go new file mode 100644 index 00000000000..95f1c19ac38 --- /dev/null +++ b/enterprise/dev/ci/scripts/app-token/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "crypto/x509" + "encoding/pem" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "time" + + "github.com/golang-jwt/jwt" + "github.com/google/go-github/v47/github" + "golang.org/x/oauth2" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func main() { + appID := flag.String("appid", os.Getenv("GITHUB_APP_ID"), "(required) github application id.") + keyPath := flag.String("keypath", os.Getenv("KEY_PATH"), "(required) path to private key file for github app.") + + flag.Parse() + + if len(*appID) == 0 || len(*keyPath) == 0 { + flag.PrintDefaults() + os.Exit(1) + } + + jwt, err := genJwtToken(*appID, *keyPath) + if err != nil { + log.Fatal(err) + } + + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: jwt}, + ) + tc := oauth2.NewClient(ctx, ts) + ghc := github.NewClient(tc) + + appToken, err := getInstallAccessToken(ctx, ghc) + if err != nil { + log.Fatal(err) + } + + fmt.Println(*appToken) +} + +func genJwtToken(appID string, keyPath string) (string, error) { + rawPem, err := ioutil.ReadFile(keyPath) + if err != nil { + return "", errors.Wrap(err, "Failed to read key file.") + } + + privPem, _ := pem.Decode(rawPem) + if privPem == nil { + return "", errors.Wrap(nil, "failed to decode PEM block containing public key") + } + priv, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) + if err != nil { + return "", errors.Wrap(err, "Failed to parse key.") + } + // Create new JWT token with 10 minute (max duration) expiry + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "iat": time.Now().Unix() - 60, + "exp": time.Now().Unix() + (10 * 60), + "iss": appID, + }) + + jwtString, err := token.SignedString(priv) + if err != nil { + return "", errors.Wrap(err, "Failed to create token.") + } + return jwtString, nil + +} + +func getInstallAccessToken(ctx context.Context, ghc *github.Client) (*string, error) { + // Get organation installation ID + orgInstallation, _, err := ghc.Apps.FindOrganizationInstallation(ctx, "sourcegraph") + if err != nil { + log.Fatal(err) + } + orgID := orgInstallation.ID + + // Create new installation token with 60 minute duraction with default read repo contents permissions + token, _, err := ghc.Apps.CreateInstallationToken(ctx, *orgID, &github.InstallationTokenOptions{ + Repositories: []string{"sourcegraph"}, + }) + if err != nil { + log.Fatal(err) + } + + return token.Token, nil +} diff --git a/enterprise/dev/ci/scripts/app-token/main_test.go b/enterprise/dev/ci/scripts/app-token/main_test.go new file mode 100644 index 00000000000..07ed00dba97 --- /dev/null +++ b/enterprise/dev/ci/scripts/app-token/main_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "flag" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/dnaeon/go-vcr/cassette" + "github.com/google/go-github/v47/github" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + "github.com/sourcegraph/sourcegraph/internal/httptestutil" +) + +var updateRecordings = flag.Bool("update-integration", false, "refresh integration test recordings") + +func TestGenJwtToken(t *testing.T) { + if os.Getenv("BUILDKITE") == "true" { + t.Skip("Skipping testing in CI environment") + } else { + appID := os.Getenv("GITHUB_APP_ID") + require.NotEmpty(t, appID, "GITHUB_APP_ID must be set.") + keyPath := os.Getenv("KEY_PATH") + require.NotEmpty(t, keyPath, "KEY_PATH must be set.") + _, err := genJwtToken(appID, keyPath) + require.NoError(t, err) + } +} + +func newTestGitHubClient(ctx context.Context, t *testing.T) (ghc *github.Client, stop func() error) { + recording := filepath.Join("tests/testdata", strings.ReplaceAll(t.Name(), " ", "-")) + recorder, err := httptestutil.NewRecorder(recording, *updateRecordings, func(i *cassette.Interaction) error { + return nil + }) + if err != nil { + t.Fatal(err) + } + + if *updateRecordings { + appID := os.Getenv("GITHUB_APP_ID") + require.NotEmpty(t, appID, "GITHUB_APP_ID must be set.") + keyPath := os.Getenv("KEY_PATH") + require.NotEmpty(t, keyPath, "KEY_PATH must be set.") + jwt, err := genJwtToken(appID, keyPath) + if err != nil { + t.Fatal(err) + httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: jwt}, + )) + recorder.SetTransport(httpClient.Transport) + } + } + return github.NewClient(&http.Client{Transport: recorder}), recorder.Stop + +} + +func TestGetInstallAccessToken(t *testing.T) { + ctx := context.Background() + + ghc, stop := newTestGitHubClient(ctx, t) + defer stop() + + _, err := getInstallAccessToken(ctx, ghc) + require.NoError(t, err) +} diff --git a/enterprise/dev/ci/scripts/app-token/tests/testdata/TestGetInstallAccessToken.yaml b/enterprise/dev/ci/scripts/app-token/tests/testdata/TestGetInstallAccessToken.yaml new file mode 100644 index 00000000000..4366bdd91b8 --- /dev/null +++ b/enterprise/dev/ci/scripts/app-token/tests/testdata/TestGetInstallAccessToken.yaml @@ -0,0 +1,113 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + Accept: + - application/vnd.github.v3+json + User-Agent: + - go-github/v47.0.0 + url: https://api.github.com/orgs/sourcegraph/installation + method: GET + response: + body: '{"id":30579796,"account":{"login":"sourcegraph","id":3979584,"node_id":"MDEyOk9yZ2FuaXphdGlvbjM5Nzk1ODQ=","avatar_url":"https://avatars.githubusercontent.com/u/3979584?v=4","gravatar_id":"","url":"https://api.github.com/users/sourcegraph","html_url":"https://github.com/sourcegraph","followers_url":"https://api.github.com/users/sourcegraph/followers","following_url":"https://api.github.com/users/sourcegraph/following{/other_user}","gists_url":"https://api.github.com/users/sourcegraph/gists{/gist_id}","starred_url":"https://api.github.com/users/sourcegraph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sourcegraph/subscriptions","organizations_url":"https://api.github.com/users/sourcegraph/orgs","repos_url":"https://api.github.com/users/sourcegraph/repos","events_url":"https://api.github.com/users/sourcegraph/events{/privacy}","received_events_url":"https://api.github.com/users/sourcegraph/received_events","type":"Organization","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/30579796/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/organizations/sourcegraph/settings/installations/30579796","app_id":244693,"app_slug":"buildkite-token-gen","target_id":3979584,"target_type":"Organization","permissions":{"contents":"read","metadata":"read"},"events":[],"created_at":"2022-10-25T12:35:08.000Z","updated_at":"2022-10-25T12:35:09.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 25 Oct 2022 14:27:36 GMT + Etag: + - W/"5fd5963d4e6164d8a0976397a1e24af2454fea5d3f31a9c100f8d6d04a8219ee" + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3; format=json + X-Github-Request-Id: + - E647:08F0:C5A40A:194F1A7:6357F258 + X-Xss-Protection: + - "0" + status: 200 OK + code: 200 + duration: "" +- request: + body: | + {"repositories":["sourcegraph"]} + form: {} + headers: + Accept: + - application/vnd.github.v3+json + Content-Type: + - application/json + User-Agent: + - go-github/v47.0.0 + url: https://api.github.com/app/installations/30579796/access_tokens + method: POST + response: + body: '{"token":"dummy","expires_at":"2022-10-25T15:27:36Z","permissions":{"contents":"read","metadata":"read"},"repository_selection":"selected","repositories":[{"id":41288708,"node_id":"MDEwOlJlcG9zaXRvcnk0MTI4ODcwOA==","name":"sourcegraph","full_name":"sourcegraph/sourcegraph","private":false,"owner":{"login":"sourcegraph","id":3979584,"node_id":"MDEyOk9yZ2FuaXphdGlvbjM5Nzk1ODQ=","avatar_url":"https://avatars.githubusercontent.com/u/3979584?v=4","gravatar_id":"","url":"https://api.github.com/users/sourcegraph","html_url":"https://github.com/sourcegraph","followers_url":"https://api.github.com/users/sourcegraph/followers","following_url":"https://api.github.com/users/sourcegraph/following{/other_user}","gists_url":"https://api.github.com/users/sourcegraph/gists{/gist_id}","starred_url":"https://api.github.com/users/sourcegraph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sourcegraph/subscriptions","organizations_url":"https://api.github.com/users/sourcegraph/orgs","repos_url":"https://api.github.com/users/sourcegraph/repos","events_url":"https://api.github.com/users/sourcegraph/events{/privacy}","received_events_url":"https://api.github.com/users/sourcegraph/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/sourcegraph/sourcegraph","description":"Universal + code search (self-hosted)","fork":false,"url":"https://api.github.com/repos/sourcegraph/sourcegraph","forks_url":"https://api.github.com/repos/sourcegraph/sourcegraph/forks","keys_url":"https://api.github.com/repos/sourcegraph/sourcegraph/keys{/key_id}","collaborators_url":"https://api.github.com/repos/sourcegraph/sourcegraph/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/sourcegraph/sourcegraph/teams","hooks_url":"https://api.github.com/repos/sourcegraph/sourcegraph/hooks","issue_events_url":"https://api.github.com/repos/sourcegraph/sourcegraph/issues/events{/number}","events_url":"https://api.github.com/repos/sourcegraph/sourcegraph/events","assignees_url":"https://api.github.com/repos/sourcegraph/sourcegraph/assignees{/user}","branches_url":"https://api.github.com/repos/sourcegraph/sourcegraph/branches{/branch}","tags_url":"https://api.github.com/repos/sourcegraph/sourcegraph/tags","blobs_url":"https://api.github.com/repos/sourcegraph/sourcegraph/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/sourcegraph/sourcegraph/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/sourcegraph/sourcegraph/git/refs{/sha}","trees_url":"https://api.github.com/repos/sourcegraph/sourcegraph/git/trees{/sha}","statuses_url":"https://api.github.com/repos/sourcegraph/sourcegraph/statuses/{sha}","languages_url":"https://api.github.com/repos/sourcegraph/sourcegraph/languages","stargazers_url":"https://api.github.com/repos/sourcegraph/sourcegraph/stargazers","contributors_url":"https://api.github.com/repos/sourcegraph/sourcegraph/contributors","subscribers_url":"https://api.github.com/repos/sourcegraph/sourcegraph/subscribers","subscription_url":"https://api.github.com/repos/sourcegraph/sourcegraph/subscription","commits_url":"https://api.github.com/repos/sourcegraph/sourcegraph/commits{/sha}","git_commits_url":"https://api.github.com/repos/sourcegraph/sourcegraph/git/commits{/sha}","comments_url":"https://api.github.com/repos/sourcegraph/sourcegraph/comments{/number}","issue_comment_url":"https://api.github.com/repos/sourcegraph/sourcegraph/issues/comments{/number}","contents_url":"https://api.github.com/repos/sourcegraph/sourcegraph/contents/{+path}","compare_url":"https://api.github.com/repos/sourcegraph/sourcegraph/compare/{base}...{head}","merges_url":"https://api.github.com/repos/sourcegraph/sourcegraph/merges","archive_url":"https://api.github.com/repos/sourcegraph/sourcegraph/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/sourcegraph/sourcegraph/downloads","issues_url":"https://api.github.com/repos/sourcegraph/sourcegraph/issues{/number}","pulls_url":"https://api.github.com/repos/sourcegraph/sourcegraph/pulls{/number}","milestones_url":"https://api.github.com/repos/sourcegraph/sourcegraph/milestones{/number}","notifications_url":"https://api.github.com/repos/sourcegraph/sourcegraph/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/sourcegraph/sourcegraph/labels{/name}","releases_url":"https://api.github.com/repos/sourcegraph/sourcegraph/releases{/id}","deployments_url":"https://api.github.com/repos/sourcegraph/sourcegraph/deployments","created_at":"2015-08-24T07:27:28Z","updated_at":"2022-10-25T08:43:48Z","pushed_at":"2022-10-25T14:27:19Z","git_url":"git://github.com/sourcegraph/sourcegraph.git","ssh_url":"git@github.com:sourcegraph/sourcegraph.git","clone_url":"https://github.com/sourcegraph/sourcegraph.git","svn_url":"https://github.com/sourcegraph/sourcegraph","homepage":"https://sourcegraph.com","size":797242,"stargazers_count":7045,"watchers_count":7045,"language":"Go","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":850,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":4673,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["code-intelligence","code-search","lsif-enabled","open-source","repo-type-main","sourcegraph"],"visibility":"public","forks":850,"open_issues":4673,"watchers":7045,"default_branch":"main"}]}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, + X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, + X-GitHub-Request-Id, Deprecation, Sunset + Cache-Control: + - public, max-age=60, s-maxage=60 + Content-Length: + - "5577" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 25 Oct 2022 14:27:36 GMT + Etag: + - '"75eee91f9a00496ee85b18131e18dbcb9e867504cbb64204b4dff8107989190f"' + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Vary: + - Accept + - Accept-Encoding, Accept, X-Requested-With + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-Github-Media-Type: + - github.v3; format=json + X-Github-Request-Id: + - E647:08F0:C5A443:194F21D:6357F258 + X-Xss-Protection: + - "0" + status: 201 Created + code: 201 + duration: "" diff --git a/go.mod b/go.mod index 6df4a836112..e83ffd38494 100644 --- a/go.mod +++ b/go.mod @@ -219,6 +219,8 @@ require ( github.com/coreos/go-iptables v0.6.0 github.com/dcadenas/pagerank v0.0.0-20171013173705-af922e3ceea8 github.com/frankban/quicktest v1.14.3 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/google/go-github/v47 v47.1.0 github.com/hashicorp/hcl v1.0.0 github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.13 github.com/prometheus/prometheus v0.37.1 diff --git a/go.sum b/go.sum index 3c79e70b748..932d42648e1 100644 --- a/go.sum +++ b/go.sum @@ -1035,6 +1035,7 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -1154,6 +1155,8 @@ github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27u github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= +github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8= +github.com/google/go-github/v47 v47.1.0/go.mod h1:VPZBXNbFSJGjyjFRUKo9vZGawTajnWzC/YjGw/oFKi0= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=