From 4ad5b0069ad77b0d9ec4fa442d421613ffb2be44 Mon Sep 17 00:00:00 2001 From: Torben Neufeldt Date: Sun, 2 Aug 2020 21:58:22 +0200 Subject: [PATCH] Lookup installationId automatically --- README.md | 5 ++-- authentication.go | 36 +++++++++++++++++------ authentication_test.go | 66 +++++++++++++++++++++++++++++++++++++----- models.go | 5 ++-- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f797439e..fbe77eb6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ If you want github-pr-resource to work with a private repository. Set `repo:full #### Github App This is useful when you are part of an organisation and do not want to share your personal access token with everyone else having access to your Secrets Manager. -Please provide `app_id`, `private_key`, `installation_id`. +Please provide `app_id` and `private_key`. We need the following permissions: - Contents - Read-only => required @@ -42,9 +42,8 @@ We need the following permissions: |-----------------------------|----------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `repository` | Yes | `itsdalmo/test-repository` | The repository to target. | | `access_token` | Auth | | A Github Access Token with repository access (required for setting status on commits). | -| `app_id` | Auth | `69592` | The Github App app id you can find on top of the overview page. | +| `app_id` | Auth | `69592` | The Github App app id can be found in the settings of your app. | | `private_key` | Auth | `-----BEGIN RSA PRIVATE KEY....` | The private key of your Github App as described [here](https://docs.github.com/en/developers/apps/authenticating-with-github-apps). | -| `installation_id` | Auth | `9912873` | On the overview page of your app you can find `Install App` on the left. After installing it you can click on a specific installation and retrieve the numeric installation id from the url. | | `v3_endpoint` | No | `https://api.github.com` | Endpoint to use for the V3 Github API (Restful). | | `v4_endpoint` | No | `https://api.github.com/graphql` | Endpoint to use for the V4 Github API (Graphql). | | `paths` | No | `["terraform/*/*.tf"]` | Only produce new versions if the PR includes changes to files that match one or more glob patterns or prefixes. | diff --git a/authentication.go b/authentication.go index 58afc767..64adf942 100644 --- a/authentication.go +++ b/authentication.go @@ -7,10 +7,15 @@ import ( "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" "net/http" + "strconv" "time" ) -type Response struct { +type InstallationResponse struct { + Id int +} + +type TokenResponse struct { Token string } @@ -42,24 +47,37 @@ func GenerateAccessToken(s *Source, now time.Time) (string, error) { } else { endpoint = "https://api.github.com" } - request, err := http.NewRequest("POST", endpoint+"/app/installations/"+s.InstallationId+"/access_tokens", nil) + + installationResponse := callApi("GET", endpoint+"/repos/"+s.Repository+"/installation", signedJwt) + var ir InstallationResponse + err = json.NewDecoder(installationResponse.Body).Decode(&ir) if err != nil { panic(err) } - request.Header.Add("Authorization", "Bearer "+signedJwt) - request.Header.Add("Accept", "application/vnd.github.machine-man-preview+json") - client := &http.Client{} - response, err := client.Do(request) + tokenResponse := callApi("POST", endpoint+"/app/installations/"+strconv.Itoa(ir.Id)+"/access_tokens", signedJwt) + + var tr TokenResponse + err = json.NewDecoder(tokenResponse.Body).Decode(&tr) if err != nil { panic(err) } - var r Response - err = json.NewDecoder(response.Body).Decode(&r) + return tr.Token, nil +} + +func callApi(method string, endpoint string, signedJwt string) *http.Response { + tokenRequest, err := http.NewRequest(method, endpoint, nil) if err != nil { panic(err) } + tokenRequest.Header.Add("Authorization", "Bearer "+signedJwt) + tokenRequest.Header.Add("Accept", "application/vnd.github.machine-man-preview+json") + client := &http.Client{} + response, err := client.Do(tokenRequest) - return r.Token, nil + if err != nil { + panic(err) + } + return response } diff --git a/authentication_test.go b/authentication_test.go index 95c9ecef..cdeb8758 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -12,7 +12,52 @@ import ( func TestGenerateAccessToken(t *testing.T) { - validResponse := ` + validInstallationResponse := ` +{ + "id": 9912873, + "account": { + "login": "github", + "id": 1, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE=", + "avatar_url": "https://github.com/images/error/hubot_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/orgs/github", + "html_url": "https://github.com/github", + "followers_url": "https://api.github.com/users/github/followers", + "following_url": "https://api.github.com/users/github/following{/other_user}", + "gists_url": "https://api.github.com/users/github/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github/subscriptions", + "organizations_url": "https://api.github.com/users/github/orgs", + "repos_url": "https://api.github.com/orgs/github/repos", + "events_url": "https://api.github.com/orgs/github/events", + "received_events_url": "https://api.github.com/users/github/received_events", + "type": "Organization", + "site_admin": false + }, + "repository_selection": "all", + "access_tokens_url": "https://api.github.com/installations/1/access_tokens", + "repositories_url": "https://api.github.com/installation/repositories", + "html_url": "https://github.com/organizations/github/settings/installations/1", + "app_id": 1, + "target_id": 1, + "target_type": "Organization", + "permissions": { + "checks": "write", + "metadata": "read", + "contents": "read" + }, + "events": [ + "push", + "pull_request" + ], + "created_at": "2018-02-09T20:51:14Z", + "updated_at": "2018-02-09T20:51:14Z", + "single_file_name": null +} +` + + validTokenResponse := ` { "token": "v1.b71be873ad96e64a84025ae7bee7694a99cb4ba9", "expires_at": "2020-06-21T00:03:29Z", @@ -27,11 +72,19 @@ func TestGenerateAccessToken(t *testing.T) { ` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/app/installations/9912873/access_tokens", r.RequestURI) - assert.Equal(t, "application/vnd.github.machine-man-preview+json", r.Header.Get("Accept")) - assert.Equal(t, "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTI2OTE5ODIsImlhdCI6MTU5MjY5MTQ0MiwiaXNzIjoiNjk1OTIifQ.H_a6i7TpaGOsaoliH_i7AT5UMwM9LqO21lEFYiZ96_H15cEF6D_kyZrcHyinP2fSC8rX_OQ-DvPsehTNTtfOhgM-nsdgg-gTzdCSlASgc00sGhw_pjBFwHpD6V1NQojc82L8SAR9Bg75g0xVlQ_dAR_Lbtmk252X_AlabRAfdSchK_GVdc3kSEbp28lc87EF7J_lFdRCNuVi1xcLFOXPmMeu4epSBf1ZMtuts28C7iqaI4QJ9keaGFug1wpL-WLDcFbvmB2nJhBYN9tArGM0ZHZ5i4EhFyFjGpBwTyo5P7WY7P3zYtz36gwgntYRtPPivcFQ-wUWuvMpL6vKd-Pp8w", r.Header.Get("Authorization")) - _, _ = fmt.Fprintln(w, validResponse) + if r.RequestURI == "/repos/itsdalmo/test-repository/installation" { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "application/vnd.github.machine-man-preview+json", r.Header.Get("Accept")) + assert.Equal(t, "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTI2OTE5ODIsImlhdCI6MTU5MjY5MTQ0MiwiaXNzIjoiNjk1OTIifQ.H_a6i7TpaGOsaoliH_i7AT5UMwM9LqO21lEFYiZ96_H15cEF6D_kyZrcHyinP2fSC8rX_OQ-DvPsehTNTtfOhgM-nsdgg-gTzdCSlASgc00sGhw_pjBFwHpD6V1NQojc82L8SAR9Bg75g0xVlQ_dAR_Lbtmk252X_AlabRAfdSchK_GVdc3kSEbp28lc87EF7J_lFdRCNuVi1xcLFOXPmMeu4epSBf1ZMtuts28C7iqaI4QJ9keaGFug1wpL-WLDcFbvmB2nJhBYN9tArGM0ZHZ5i4EhFyFjGpBwTyo5P7WY7P3zYtz36gwgntYRtPPivcFQ-wUWuvMpL6vKd-Pp8w", r.Header.Get("Authorization")) + _, _ = fmt.Fprintln(w, validInstallationResponse) + } + + if r.RequestURI == "/app/installations/9912873/access_tokens" { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "application/vnd.github.machine-man-preview+json", r.Header.Get("Accept")) + assert.Equal(t, "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTI2OTE5ODIsImlhdCI6MTU5MjY5MTQ0MiwiaXNzIjoiNjk1OTIifQ.H_a6i7TpaGOsaoliH_i7AT5UMwM9LqO21lEFYiZ96_H15cEF6D_kyZrcHyinP2fSC8rX_OQ-DvPsehTNTtfOhgM-nsdgg-gTzdCSlASgc00sGhw_pjBFwHpD6V1NQojc82L8SAR9Bg75g0xVlQ_dAR_Lbtmk252X_AlabRAfdSchK_GVdc3kSEbp28lc87EF7J_lFdRCNuVi1xcLFOXPmMeu4epSBf1ZMtuts28C7iqaI4QJ9keaGFug1wpL-WLDcFbvmB2nJhBYN9tArGM0ZHZ5i4EhFyFjGpBwTyo5P7WY7P3zYtz36gwgntYRtPPivcFQ-wUWuvMpL6vKd-Pp8w", r.Header.Get("Authorization")) + _, _ = fmt.Fprintln(w, validTokenResponse) + } })) defer ts.Close() @@ -54,7 +107,6 @@ func TestGenerateAccessToken(t *testing.T) { Repository: "itsdalmo/test-repository", AppId: "69592", PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAv6oJaa+0BCOT0dITtJK6oPfrE2R0Iynofj/a/vq4rfAw2MJp\nXA5l6WbwmQhevSm8sIYNb2T32qLyRsVXwBo1J8ovIoTXiPdsV41D19tytjctnf+u\n0LncTo2JJR3ik7/Ynu5Id4zjnsn1pYqwLX4EFxgCuEKwdZutPyiY2J7wATfsTtOJ\nsLF8idQijG23i5Obs6AWCZcHOhvdgfYAUOxLv2WRkCG5O1aXYa6nqVn0AgRgKjaJ\nnAENEoG7O9OEWIcUiG30riQouxMHfj0bATCvYoj7a2tvn4CUqk+SBODyh3Fvi0oC\na/NCBYFeK/5uUNyXQHqWy7xEVRef5lC0XbrUVwIDAQABAoIBAFDdqhkIQ/iXFjAp\n5ZyDZ/CwiWNmN8X6UZiq0nhQSolA1SsvY4qunHsMrqiyql4/dNg5xwNf419A7t3D\nN5HavOCr4pU63UFxuyl5dc1mTpDo2PtXvGdec8BE4T9iy40xHXF48eRW8la1uUn+\nKPUYvRsNS2B46sDETSVfuJV1AahRN6aD2WnzQ7wB+S/mqsPXqy+S2zobnU70Wzmt\nhYQzsuIY+BkfOCS565guYvJt66wRGi/NsnybC6z3iRZxZygtKorMKXPjmtdoyCZ3\njkOHhXV5XH8Ldut+1mycg+6c+dTZ9RTrAzo4ouofptm9+ZNlv1aK7HrDDKYQ/x/z\n70hhIfkCgYEA7aNt7db3S6sTEA+yC5DLxxkFIK7K8qxdP6vE087fdHO+ik1LvyI+\ni5Wqj/fT2d/lYR+cbH/zy2JBy5RWfdVJ9HBW/eZ9RqQx8ry7lE5MqbwU6y+d2bCF\nAjffx3yJK1aljuVLdGeu8abYsusUQUhbslZJNg6Ar7z+FUHaChJI0KsCgYEAznk4\nx+5PtvIWj6aXTJiqtofnOtHXXxrq5alzBErvDBHHzP9/ZCucHxJZ4zqHWduj2+9b\nJUgk2BH3+wFMVKpcdTld2iTFWGaFsArTJkE4SBQGx3zcdsoESOnu/DrOTFNu+LiV\nhLzb9CHKM91fIet8MYYxKiyH09+Mi7xBtw8WQwUCgYAH/tCrCOmHHTll9/E4nGWO\nzFO01syzP4NfqgrUSYiRJXfKtXEP/Dn4fk+fymnRUcwo6WRc7i0osaSfEd2bHDsB\nw2nZ3xBl+Q5JKXpyMfQ4XcCibRa1hU/kVDbuQk1nLOIjHandP8POE5wE4Q3saF/V\nbzvFWtWPlB9EXdPVNOpIQwKBgHkJEsIQ72XdUGBxVewu6pQJ4wDWFhzIWL68sJHp\no2w92BRSCkmcTu7gARV1L/b7DHlXPOUD/6UyE15vCmHvZDfLozrHp3AE2YWzMsgQ\nH4ARTVAP3+U603wytkfh6SFRH5JqEiw30fCxBimVMbleo/UcJyID7LPFLkyT1SoM\njA5JAoGACEhaHTXWFYXv+eTJUXFcwhDZ5sRvQYRCTLGSv746lr+SpZ02bEcRvaTH\nv0K1Hph6OzhcCO27VdimngnzsoXoa2OXicWpdjX3hqhXMvwspe9a1y9u7LPZwqJH\n9Kc7VaZP1iS4EIxEc+qx38HqdeiUBcqTMRam6k3mSwactBKCKDI=\n-----END RSA PRIVATE KEY-----\n", - InstallationId: "9912873", V3Endpoint: ts.URL, }, expectedAccessToken: "v1.b71be873ad96e64a84025ae7bee7694a99cb4ba9", diff --git a/models.go b/models.go index 126b09a2..e37adfbf 100644 --- a/models.go +++ b/models.go @@ -15,7 +15,6 @@ type Source struct { AccessToken string `json:"access_token"` AppId string `json:"app_id"` PrivateKey string `json:"private_key"` - InstallationId string `json:"installation_id"` V3Endpoint string `json:"v3_endpoint"` V4Endpoint string `json:"v4_endpoint"` Paths []string `json:"paths"` @@ -34,8 +33,8 @@ type Source struct { // Validate the source configuration. func (s *Source) Validate() error { - if s.AccessToken == "" && (s.AppId == "" || s.PrivateKey == "" || s.InstallationId == "") { - return errors.New("access_token or app_id and private_key and installation_id must be set") + if s.AccessToken == "" && (s.AppId == "" || s.PrivateKey == "") { + return errors.New("access_token or app_id and private_key must be set") } if s.Repository == "" { return errors.New("repository must be set")