diff --git a/cmd/server.go b/cmd/server.go index caa1351f7..e9326cd1b 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -90,6 +90,7 @@ const ( GHAppKeyFlag = "gh-app-key" GHAppKeyFileFlag = "gh-app-key-file" GHAppSlugFlag = "gh-app-slug" + GHInstallationIDFlag = "gh-installation-id" GHOrganizationFlag = "gh-org" GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec @@ -618,6 +619,13 @@ var int64Flags = map[string]int64Flag{ description: "GitHub App Id. If defined, initializes the GitHub client with app-based credentials", defaultValue: 0, }, + GHInstallationIDFlag: { + description: "GitHub Installation Id. If defined, initializes the GitHub client with app-based credentials " + + "using this specific GitHub Application Installation ID, otherwise it attempts to auto-detect it. " + + "Note that this value must be set if you want to have one App and multiple installations of that same " + + "application.", + defaultValue: 0, + }, } // ValidLogLevels are the valid log levels that can be set diff --git a/cmd/server_test.go b/cmd/server_test.go index 1d5ff3c77..db181f93a 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -91,6 +91,7 @@ var testFlags = map[string]interface{}{ GHAppKeyFlag: "", GHAppKeyFileFlag: "", GHAppSlugFlag: "atlantis", + GHInstallationIDFlag: int64(0), GHOrganizationFlag: "", GHWebhookSecretFlag: "secret", GiteaBaseURLFlag: "http://localhost", @@ -746,6 +747,21 @@ func TestExecute_GithubApp(t *testing.T) { Equals(t, int64(1), passedConfig.GithubAppID) } +func TestExecute_GithubAppWithInstallationID(t *testing.T) { + t.Log("Should pass the installation ID to the config.") + c := setup(map[string]interface{}{ + GHAppKeyFlag: testdata.GithubPrivateKey, + GHAppIDFlag: "1", + GHInstallationIDFlag: "2", + RepoAllowlistFlag: "*", + }, t) + err := c.Execute() + Ok(t, err) + + Equals(t, int64(1), passedConfig.GithubAppID) + Equals(t, int64(2), passedConfig.GithubInstallationID) +} + func TestExecute_GiteaUser(t *testing.T) { t.Log("Should remove the @ from the gitea username if it's passed.") c := setup(map[string]interface{}{ diff --git a/e2e/go.mod b/e2e/go.mod index b1d719e5a..786275678 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -7,7 +7,4 @@ require ( github.com/hashicorp/go-multierror v1.1.1 ) -require ( - github.com/google/go-querystring v1.1.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect -) +require github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.mod b/go.mod index 977cbcb04..279aa2394 100644 --- a/go.mod +++ b/go.mod @@ -47,19 +47,15 @@ require ( github.com/xanzy/go-gitlab v0.102.0 go.etcd.io/bbolt v1.3.10 go.uber.org/zap v1.27.0 - golang.org/x/term v0.19.0 - golang.org/x/text v0.14.0 + golang.org/x/term v0.20.0 + golang.org/x/text v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/agext/levenshtein v1.2.3 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/hashicorp/hcl/v2 v2.20.1 - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect go.uber.org/atomic v1.11.0 // indirect ) @@ -82,6 +78,8 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -92,15 +90,17 @@ require ( github.com/gookit/gsr v0.1.0 // indirect github.com/gookit/slog v0.5.5 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -131,15 +131,15 @@ require ( github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zclconf/go-cty v1.14.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.14.0 // indirect + golang.org/x/tools v0.21.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 24c36a6e5..4bdbab874 100644 --- a/go.sum +++ b/go.sum @@ -230,9 +230,8 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-getter/v2 v2.2.1 h1:2JXqPZs1Jej67RtdTi0YZaEB2hEFB3fkBA4cPYKQwFQ= @@ -486,6 +485,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -522,6 +523,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -560,6 +563,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -583,6 +588,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -632,6 +639,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -640,6 +649,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -652,6 +663,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -701,6 +714,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 439e55d48..d2ba3653b 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -687,6 +687,21 @@ and set `--autoplan-modules` to `false`. Hostname of your GitHub Enterprise installation. If using [GitHub.com](https://github.com), don't set. Defaults to `github.com`. +### `--gh-installation-id` + + ```bash + atlantis server --gh-installation-id="123" + # or + ATLANTIS_GH_INSTALLATION_ID="123" + ``` + +The installation ID of a specific instance of a GitHub application. Normally this value is +derived by querying GitHub for the list of installations of the ID supplied via `--gh-app-id` and selecting +the first one found and where multiple installations results in an error. Use this flag if you have multiple +instances of Atlantis but you want to use a single already-installed GitHub app for all of them. You would normally do this if +you are running a proxy as your single GitHub application that will proxy to an appropriate Atlantis instance +based on the organization or user that triggered the webhook. + ### `--gh-org` ```bash diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index c2e070cfe..d298b2cee 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -4,11 +4,12 @@ package events import ( + "reflect" + "time" + pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" - "reflect" - "time" ) type MockWorkingDir struct { diff --git a/server/events/vcs/github_credentials.go b/server/events/vcs/github_credentials.go index 116b5bead..d1d59bffa 100644 --- a/server/events/vcs/github_credentials.go +++ b/server/events/vcs/github_credentials.go @@ -71,7 +71,7 @@ type GithubAppCredentials struct { Key []byte Hostname string apiURL *url.URL - installationID int64 + InstallationID int64 tr *ghinstallation.Transport AppSlug string } @@ -122,8 +122,8 @@ func (c *GithubAppCredentials) GetToken() (string, error) { } func (c *GithubAppCredentials) getInstallationID() (int64, error) { - if c.installationID != 0 { - return c.installationID, nil + if c.InstallationID != 0 { + return c.InstallationID, nil } tr := http.DefaultTransport @@ -148,8 +148,8 @@ func (c *GithubAppCredentials) getInstallationID() (int64, error) { return 0, fmt.Errorf("wrong number of installations, expected 1, found %d", len(installations)) } - c.installationID = installations[0].GetID() - return c.installationID, nil + c.InstallationID = installations[0].GetID() + return c.InstallationID, nil } func (c *GithubAppCredentials) transport() (*ghinstallation.Transport, error) { diff --git a/server/events/vcs/github_credentials_test.go b/server/events/vcs/github_credentials_test.go index f6604adfa..f5a4d33f0 100644 --- a/server/events/vcs/github_credentials_test.go +++ b/server/events/vcs/github_credentials_test.go @@ -69,3 +69,40 @@ func TestGithubClient_AppAuthentication(t *testing.T) { t.Errorf("app token was not cached: %q != %q", token, newToken) } } + +func TestGithubClient_MultipleAppAuthentication(t *testing.T) { + logger := logging.NewNoopLogger(t) + defer disableSSLVerification()() + testServer, err := testdata.GithubMultipleAppTestServer(t) + Ok(t, err) + + anonCreds := &vcs.GithubAnonymousCredentials{} + anonClient, err := vcs.NewGithubClient(testServer, anonCreds, vcs.GithubConfig{}, logging.NewNoopLogger(t)) + Ok(t, err) + tempSecrets, err := anonClient.ExchangeCode(logger, "good-code") + Ok(t, err) + + appCreds := &vcs.GithubAppCredentials{ + AppID: tempSecrets.ID, + InstallationID: 1, + Key: []byte(testdata.GithubPrivateKey), + Hostname: testServer, + } + _, err = vcs.NewGithubClient(testServer, appCreds, vcs.GithubConfig{}, logging.NewNoopLogger(t)) + Ok(t, err) + + token, err := appCreds.GetToken() + Ok(t, err) + + newToken, err := appCreds.GetToken() + Ok(t, err) + + user, err := appCreds.GetUser() + Ok(t, err) + + Assert(t, user == "", "user should be empty") + + if token != newToken { + t.Errorf("app token was not cached: %q != %q", token, newToken) + } +} diff --git a/server/events/vcs/testdata/fixtures.go b/server/events/vcs/testdata/fixtures.go index 94926edba..2872892f2 100644 --- a/server/events/vcs/testdata/fixtures.go +++ b/server/events/vcs/testdata/fixtures.go @@ -496,6 +496,79 @@ var githubAppInstallationJSON = `[ } ]` +var githubAppMultipleInstallationJSON = `[ + { + "id": 1, + "account": { + "login": "github", + "id": 1, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE=", + "url": "https://api.github.com/orgs/github", + "repos_url": "https://api.github.com/orgs/github/repos", + "events_url": "https://api.github.com/orgs/github/events", + "hooks_url": "https://api.github.com/orgs/github/hooks", + "issues_url": "https://api.github.com/orgs/github/issues", + "members_url": "https://api.github.com/orgs/github/members{/member}", + "public_members_url": "https://api.github.com/orgs/github/public_members{/member}", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "description": "A great organization" + }, + "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": { + "metadata": "read", + "contents": "read", + "issues": "write", + "single_file": "write" + }, + "events": [ + "push", + "pull_request" + ], + "single_file_name": "config.yml", + "repository_selection": "selected" + }, + { + "id": 2, + "account": { + "login": "github", + "id": 1, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE=", + "url": "https://api.github.com/orgs/github", + "repos_url": "https://api.github.com/orgs/github/repos", + "events_url": "https://api.github.com/orgs/github/events", + "hooks_url": "https://api.github.com/orgs/github/hooks", + "issues_url": "https://api.github.com/orgs/github/issues", + "members_url": "https://api.github.com/orgs/github/members{/member}", + "public_members_url": "https://api.github.com/orgs/github/public_members{/member}", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "description": "A great organization" + }, + "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": { + "metadata": "read", + "contents": "read", + "issues": "write", + "single_file": "write" + }, + "events": [ + "push", + "pull_request" + ], + "single_file_name": "config.yml", + "repository_selection": "selected" + } +]` + // nolint: gosec var githubAppTokenJSON = `{ "token": "some-token", @@ -741,3 +814,58 @@ func GithubAppTestServer(t *testing.T) (string, error) { return testServerURL.Host, err } + +func GithubMultipleAppTestServer(t *testing.T) (string, error) { + counter := 0 + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/v3/app-manifests/good-code/conversions": + encodedKey := strings.Join(strings.Split(GithubPrivateKey, "\n"), "\\n") + appInfo := fmt.Sprintf(githubConversionJSON, encodedKey) + w.Write([]byte(appInfo)) // nolint: errcheck + // https://developer.github.com/v3/apps/#list-installations + case "/api/v3/app/installations": + token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1) + if err := validateGithubToken(token); err != nil { + w.WriteHeader(403) + w.Write([]byte("Invalid token")) // nolint: errcheck + return + } + + w.Write([]byte(githubAppMultipleInstallationJSON)) // nolint: errcheck + return + case "/api/v3/apps/some-app": + token := strings.Replace(r.Header.Get("Authorization"), "token ", "", 1) + + // token is taken from githubAppTokenJSON + if token != "some-token" { + w.WriteHeader(403) + w.Write([]byte("Invalid installation token")) // nolint: errcheck + return + } + w.Write([]byte(githubAppJSON)) // nolint: errcheck + return + case "/api/v3/app/installations/1/access_tokens": + token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1) + if err := validateGithubToken(token); err != nil { + w.WriteHeader(403) + w.Write([]byte("Invalid token")) // nolint: errcheck + return + } + + appToken := fmt.Sprintf(githubAppTokenJSON, counter) + counter++ + w.Write([]byte(appToken)) // nolint: errcheck + return + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + + testServerURL, err := url.Parse(testServer.URL) + + return testServerURL.Host, err +} diff --git a/server/server.go b/server/server.go index 215b5dd02..859f42323 100644 --- a/server/server.go +++ b/server/server.go @@ -239,18 +239,20 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { return nil, err } githubCredentials = &vcs.GithubAppCredentials{ - AppID: userConfig.GithubAppID, - Key: privateKey, - Hostname: userConfig.GithubHostname, - AppSlug: userConfig.GithubAppSlug, + AppID: userConfig.GithubAppID, + InstallationID: userConfig.GithubInstallationID, + Key: privateKey, + Hostname: userConfig.GithubHostname, + AppSlug: userConfig.GithubAppSlug, } githubAppEnabled = true } else if userConfig.GithubAppID != 0 && userConfig.GithubAppKey != "" { githubCredentials = &vcs.GithubAppCredentials{ - AppID: userConfig.GithubAppID, - Key: []byte(userConfig.GithubAppKey), - Hostname: userConfig.GithubHostname, - AppSlug: userConfig.GithubAppSlug, + AppID: userConfig.GithubAppID, + InstallationID: userConfig.GithubInstallationID, + Key: []byte(userConfig.GithubAppKey), + Hostname: userConfig.GithubHostname, + AppSlug: userConfig.GithubAppSlug, } githubAppEnabled = true } diff --git a/server/user_config.go b/server/user_config.go index 8109a3027..580162ea9 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -57,6 +57,7 @@ type UserConfig struct { GithubAppKey string `mapstructure:"gh-app-key"` GithubAppKeyFile string `mapstructure:"gh-app-key-file"` GithubAppSlug string `mapstructure:"gh-app-slug"` + GithubInstallationID int64 `mapstructure:"gh-installation-id"` GithubTeamAllowlist string `mapstructure:"gh-team-allowlist"` GiteaBaseURL string `mapstructure:"gitea-base-url"` GiteaToken string `mapstructure:"gitea-token"`