From 7ef65dedf2fd33649a71d47a5bf82f448a3b5664 Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Fri, 9 Sep 2022 09:32:08 +0000 Subject: [PATCH 1/7] Add a --actions-admin-user flag Same as in here https://github.com/github/codeql-action-sync-tool/blob/33463970b845ac4bb9e4cae6adc2a8b7cd6f6142/internal/push/push.go#L106-L113 Add a warning about the missing site_admin scope in the token Add a way to ignore the actions admin impersonation. --- src/push.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/push.go b/src/push.go index 798f161..5f32537 100644 --- a/src/push.go +++ b/src/push.go @@ -17,9 +17,14 @@ import ( "golang.org/x/oauth2" ) +const enterpriseAegisVersionHeaderValue = "GitHub AE" +const enterpriseAPIPath = "/api/v3" +const enterpriseVersionHeaderKey = "X-GitHub-Enterprise-Version" +const xOAuthScopesHeader = "X-OAuth-Scopes" + type PushOnlyFlags struct { - BaseURL, Token string - DisableGitAuth bool + BaseURL, Token, ActionsAdminUser string + DisableGitAuth bool } type PushFlags struct { @@ -34,6 +39,7 @@ func (f *PushFlags) Init(cmd *cobra.Command) { func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.BaseURL, "destination-url", "", "URL of GHES instance") + cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user. Pass '-' to disable the impersonation") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") } @@ -53,7 +59,62 @@ func (f *PushOnlyFlags) Validate() Validations { return validations } +func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error) { + fmt.Printf("Getting an impersonation token for `%s`..\n", flags.ActionsAdminUser) + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: flags.Token}) + tc := oauth2.NewClient(ctx, ts) + ghClient, err := github.NewEnterpriseClient(flags.BaseURL, flags.BaseURL, tc) + if err != nil { + return "", errors.Wrap(err, "error creating enterprise client") + } + + rootRequest, err := ghClient.NewRequest("GET", enterpriseAPIPath, nil) + if err != nil { + return "", errors.Wrap(err, "Error constructing request for GitHub Enterprise client.") + } + rootResponse, err := ghClient.Do(ctx, rootRequest, nil) + if err != nil { + return "", errors.Wrap(err, "Error checking connectivity for GitHub Enterprise client.") + } + + scopesHeader := rootResponse.Header.Get(xOAuthScopesHeader) + fmt.Printf("These are the scopes we have for the current token `%s`..\n", scopesHeader) + + isAE := rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue + minimumRepositoryScope := "public_repo" + if isAE { + // the default repository scope for non-ae instances is 'public_repo' + // while it is `repo` for ae. + minimumRepositoryScope = "repo" + fmt.Printf("Running against GitHub AE, changing the repository scope to '%s'..\n", minimumRepositoryScope) + } else { + if !strings.Contains(scopesHeader, "site_admin") { + fmt.Printf("The current token doesn't have the `site_admin` scope. The impersonation request for GHES requres the `site_admin` permission to be able to impersonate. For GitHub AE it's not required.") + } + } + + impersonationToken, _, err := ghClient.Admin.CreateUserImpersonation(ctx, flags.ActionsAdminUser, &github.ImpersonateUserOptions{Scopes: []string{minimumRepositoryScope, "workflow"}}) + if err != nil { + return "", errors.Wrap(err, "Failed to impersonate Actions admin user.") + } + + fmt.Printf("Got the impersonation token for `%s`..\n", flags.ActionsAdminUser) + + return impersonationToken.GetToken(), nil +} + func Push(ctx context.Context, flags *PushFlags) error { + if flags.ActionsAdminUser != "-" { + var token, err = GetImpersonationToken(ctx, flags) + if err != nil { + return errors.Wrap(err, "error obtaining the impersonation token") + } + + // Override the initial token with the one that we got in the exchange + flags.Token = token + } + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: flags.Token}) tc := oauth2.NewClient(ctx, ts) ghClient, err := github.NewEnterpriseClient(flags.BaseURL, flags.BaseURL, tc) From e4525fb4aacb0653a7c3129a7f81259811ca10ce Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Mon, 5 Sep 2022 15:18:39 +0000 Subject: [PATCH 2/7] Add dummy routes to the test stub to mimic the github api --- test/github.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/github.go b/test/github.go index 39bd814..f767798 100644 --- a/test/github.go +++ b/test/github.go @@ -26,6 +26,10 @@ func main() { r := mux.NewRouter() r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {}) + r.HandleFunc("/api/v3", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("x-github-enterprise-version", "GitHub AE") + }) + r.HandleFunc("/api/v3/user", func(w http.ResponseWriter, r *http.Request) { currentUser := github.User{Login: &authenticatedLogin} b, _ := json.Marshal(currentUser) @@ -35,6 +39,17 @@ func main() { } }) + r.HandleFunc("/api/v3/admin/users/actions-admin/authorizations", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("x-github-enterprise-version", "GitHub AE") + token := "token" + auth := github.Authorization{Token: &token} + b, _ := json.Marshal(auth) + _, err := w.Write(b) + if err != nil { + panic(err) + } + }).Methods("POST") + r.HandleFunc("/api/v3/admin/organizations", func(w http.ResponseWriter, r *http.Request) { b, err := ioutil.ReadAll(r.Body) if err != nil { From 669526d2392503670febd00c15179500db1c489e Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Tue, 6 Sep 2022 07:34:27 +0000 Subject: [PATCH 3/7] Add the compiled binary to git ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 59ea580..375255d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ _tools/bin bin test/tmp -dist \ No newline at end of file +dist +actions-sync From c32265a4df930b62f3d25f635bbf571f7d1613c9 Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Tue, 6 Sep 2022 07:47:46 +0000 Subject: [PATCH 4/7] Add actions-admin-user to readme --- README.md | 4 ++++ src/push.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec111ab..41ad66c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ When there are machines which have access to both the public internet and the GH **Arguments:** +- `actions-admin-user` _(optional)_ + The name of the Actions admin user, which will be used for updating the chosen action. If not specified `actions-admin` will be used. To disable the impersonation pass `-` as the value. - `cache-dir` _(required)_ The directory in which to cache repositories as they are synced. This speeds up re-syncing. - `destination-url` _(required)_ @@ -84,6 +86,8 @@ When no machine has access to both the public internet and the GHES instance: **Arguments:** +- `actions-admin-user` _(optional)_ + The name of the Actions admin user, which will be used for updating the chosen action. If not specified `actions-admin` will be used. To disable the impersonation pass `-` as the value. - `cache-dir` _(required)_ The directory containing the repositories fetched using the `pull` command. - `destination-url` _(required)_ diff --git a/src/push.go b/src/push.go index 5f32537..75fd460 100644 --- a/src/push.go +++ b/src/push.go @@ -39,7 +39,7 @@ func (f *PushFlags) Init(cmd *cobra.Command) { func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.BaseURL, "destination-url", "", "URL of GHES instance") - cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user. Pass '-' to disable the impersonation") + cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user (pass '-' to disable the impersonation)") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") } From a7d588cf7cf387d5ad02abe65f65e873ea688770 Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Tue, 20 Sep 2022 09:48:29 +0000 Subject: [PATCH 5/7] Don't enable the impersonation by default. * Require the impersonation to be explicit with the --actions-admin-user flag * Reword the log messages to look the same way the existing messages are --- README.md | 4 ++-- src/push.go | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 41ad66c..28882b1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ When there are machines which have access to both the public internet and the GH **Arguments:** - `actions-admin-user` _(optional)_ - The name of the Actions admin user, which will be used for updating the chosen action. If not specified `actions-admin` will be used. To disable the impersonation pass `-` as the value. + The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. - `cache-dir` _(required)_ The directory in which to cache repositories as they are synced. This speeds up re-syncing. - `destination-url` _(required)_ @@ -87,7 +87,7 @@ When no machine has access to both the public internet and the GHES instance: **Arguments:** - `actions-admin-user` _(optional)_ - The name of the Actions admin user, which will be used for updating the chosen action. If not specified `actions-admin` will be used. To disable the impersonation pass `-` as the value. + The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. - `cache-dir` _(required)_ The directory containing the repositories fetched using the `pull` command. - `destination-url` _(required)_ diff --git a/src/push.go b/src/push.go index 75fd460..dce12ff 100644 --- a/src/push.go +++ b/src/push.go @@ -39,7 +39,7 @@ func (f *PushFlags) Init(cmd *cobra.Command) { func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.BaseURL, "destination-url", "", "URL of GHES instance") - cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user (pass '-' to disable the impersonation)") + cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "", "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'.") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") } @@ -60,7 +60,7 @@ func (f *PushOnlyFlags) Validate() Validations { } func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error) { - fmt.Printf("Getting an impersonation token for `%s`..\n", flags.ActionsAdminUser) + fmt.Printf("getting an impersonation token for `%s` ...\n", flags.ActionsAdminUser) ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: flags.Token}) tc := oauth2.NewClient(ctx, ts) @@ -71,15 +71,15 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error rootRequest, err := ghClient.NewRequest("GET", enterpriseAPIPath, nil) if err != nil { - return "", errors.Wrap(err, "Error constructing request for GitHub Enterprise client.") + return "", errors.Wrap(err, "error constructing request for GitHub Enterprise client.") } rootResponse, err := ghClient.Do(ctx, rootRequest, nil) if err != nil { - return "", errors.Wrap(err, "Error checking connectivity for GitHub Enterprise client.") + return "", errors.Wrap(err, "error checking connectivity for GitHub Enterprise client.") } scopesHeader := rootResponse.Header.Get(xOAuthScopesHeader) - fmt.Printf("These are the scopes we have for the current token `%s`..\n", scopesHeader) + fmt.Printf("these are the scopes we have for the current token `%s` ...\n", scopesHeader) isAE := rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue minimumRepositoryScope := "public_repo" @@ -87,10 +87,10 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error // the default repository scope for non-ae instances is 'public_repo' // while it is `repo` for ae. minimumRepositoryScope = "repo" - fmt.Printf("Running against GitHub AE, changing the repository scope to '%s'..\n", minimumRepositoryScope) + fmt.Printf("running against GitHub AE, changing the repository scope to '%s' ...\n", minimumRepositoryScope) } else { if !strings.Contains(scopesHeader, "site_admin") { - fmt.Printf("The current token doesn't have the `site_admin` scope. The impersonation request for GHES requres the `site_admin` permission to be able to impersonate. For GitHub AE it's not required.") + fmt.Printf("the current token doesn't have the `site_admin` scope. The impersonation request for GHES requres the `site_admin` permission to be able to impersonate. For GitHub AE it's not required.") } } @@ -99,13 +99,13 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error return "", errors.Wrap(err, "Failed to impersonate Actions admin user.") } - fmt.Printf("Got the impersonation token for `%s`..\n", flags.ActionsAdminUser) + fmt.Printf("got the impersonation token for `%s` ...\n", flags.ActionsAdminUser) return impersonationToken.GetToken(), nil } func Push(ctx context.Context, flags *PushFlags) error { - if flags.ActionsAdminUser != "-" { + if flags.ActionsAdminUser != "" { var token, err = GetImpersonationToken(ctx, flags) if err != nil { return errors.Wrap(err, "error obtaining the impersonation token") @@ -113,6 +113,8 @@ func Push(ctx context.Context, flags *PushFlags) error { // Override the initial token with the one that we got in the exchange flags.Token = token + } else { + fmt.Print("not using impersonation for the requests \n") } ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: flags.Token}) From 37946dfc91d4a7ac4265a1d005ad8f9551cbfd45 Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Wed, 21 Sep 2022 11:59:16 +0000 Subject: [PATCH 6/7] Require site_admin for the impersonation logic Also, fixed casing in the text literals --- src/push.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/push.go b/src/push.go index dce12ff..e678911 100644 --- a/src/push.go +++ b/src/push.go @@ -81,6 +81,10 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error scopesHeader := rootResponse.Header.Get(xOAuthScopesHeader) fmt.Printf("these are the scopes we have for the current token `%s` ...\n", scopesHeader) + if !strings.Contains(scopesHeader, "site_admin") { + return "", errors.Wrap(err, "the current token doesn't have the `site_admin` scope, the impersonation function requires the `site_admin` permission to be able to impersonate.") + } + isAE := rootResponse.Header.Get(enterpriseVersionHeaderKey) == enterpriseAegisVersionHeaderValue minimumRepositoryScope := "public_repo" if isAE { @@ -88,15 +92,11 @@ func GetImpersonationToken(ctx context.Context, flags *PushFlags) (string, error // while it is `repo` for ae. minimumRepositoryScope = "repo" fmt.Printf("running against GitHub AE, changing the repository scope to '%s' ...\n", minimumRepositoryScope) - } else { - if !strings.Contains(scopesHeader, "site_admin") { - fmt.Printf("the current token doesn't have the `site_admin` scope. The impersonation request for GHES requres the `site_admin` permission to be able to impersonate. For GitHub AE it's not required.") - } } impersonationToken, _, err := ghClient.Admin.CreateUserImpersonation(ctx, flags.ActionsAdminUser, &github.ImpersonateUserOptions{Scopes: []string{minimumRepositoryScope, "workflow"}}) if err != nil { - return "", errors.Wrap(err, "Failed to impersonate Actions admin user.") + return "", errors.Wrap(err, "failed to impersonate Actions admin user.") } fmt.Printf("got the impersonation token for `%s` ...\n", flags.ActionsAdminUser) From 4b5fb2c8adacf2a58ac72f0bf5054f2b82509093 Mon Sep 17 00:00:00 2001 From: Denys Zhuravel Date: Wed, 21 Sep 2022 12:05:27 +0000 Subject: [PATCH 7/7] Add a note about the site_admin scope * add a note to the readme * add a note to the autogenerated cli help --- README.md | 6 +++--- src/push.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28882b1..2699269 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ When there are machines which have access to both the public internet and the GH **Arguments:** - `actions-admin-user` _(optional)_ - The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. + The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. Note that `site_admin` scope is required in the token for the impersonation to work. - `cache-dir` _(required)_ The directory in which to cache repositories as they are synced. This speeds up re-syncing. - `destination-url` _(required)_ @@ -87,7 +87,7 @@ When no machine has access to both the public internet and the GHES instance: **Arguments:** - `actions-admin-user` _(optional)_ - The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. + The name of the Actions admin user, which will be used for updating the chosen action. To use the default user, pass `actions-admin`. If not set, the impersonation is disabled. Note that `site_admin` scope is required in the token for the impersonation to work. - `cache-dir` _(required)_ The directory containing the repositories fetched using the `pull` command. - `destination-url` _(required)_ @@ -108,7 +108,7 @@ When no machine has access to both the public internet and the GHES instance: ## Destination token scopes -When creating a personal access token include the `repo` and `workflow` scopes. Include the `site_admin` scope (optional) if you want organizations to be created as necessary. +When creating a personal access token include the `repo` and `workflow` scopes. Include the `site_admin` scope (optional) if you want organizations to be created as necessary or you want to use the impersonation logic for the `push` or `sync` commands. ## Contributing diff --git a/src/push.go b/src/push.go index e678911..ecfcfc6 100644 --- a/src/push.go +++ b/src/push.go @@ -39,7 +39,7 @@ func (f *PushFlags) Init(cmd *cobra.Command) { func (f *PushOnlyFlags) Init(cmd *cobra.Command) { cmd.Flags().StringVar(&f.BaseURL, "destination-url", "", "URL of GHES instance") - cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "", "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'.") + cmd.Flags().StringVar(&f.ActionsAdminUser, "actions-admin-user", "", "A user to impersonate for the push requests. To use the default name, pass 'actions-admin'. Note that the site_admin scope in the token is required for the impersonation to work.") cmd.Flags().StringVar(&f.Token, "destination-token", "", "Token to access API on GHES instance") cmd.Flags().BoolVar(&f.DisableGitAuth, "disable-push-git-auth", false, "Disables git authentication whilst pushing") }