Skip to content

Commit ef02b83

Browse files
Use an impersonation token to push to GitHub Enterprise if pushing directly is not allowed.
Co-authored-by: Simon Engledew <[email protected]>
1 parent 993f67a commit ef02b83

File tree

5 files changed

+33
-26
lines changed

5 files changed

+33
-26
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ From a machine with access to both GitHub.com and GitHub Enterprise Server use t
2020

2121
**Required Arguments:**
2222
* `--destination-url` - The URL of the GitHub Enterprise Server instance to push the Action to.
23-
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.
23+
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist or that you are not an owner of, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.
2424

2525
**Optional Arguments:**
2626
* `--cache-dir` - A temporary directory in which to store data downloaded from GitHub.com before it is uploaded to GitHub Enterprise Server. If not specified a directory next to the sync tool will be used.
2727
* `--source-token` - A token to access the API of GitHub.com. This is normally not required, but can be provided if you have issues with API rate limiting. The token does not need to have any scopes.
2828
* `--destination-repository` - The name of the repository in which to create or update the CodeQL Action. If not specified `github/codeql-action` will be used.
29+
* `--actions-admin-user` - The name of the Actions admin user, which will be used if you are updating the bundled CodeQL Action. If not specified `actions-admin` will be used.
2930
* `--force` - By default the tool will not overwrite existing repositories. Providing this flag will allow it to.
3031
* `--push-ssh` - Push Git contents over SSH rather than HTTPS. To use this option you must have SSH access to your GitHub Enterprise instance configured.
3132

@@ -42,11 +43,12 @@ Now use the `./codeql-action-sync push` command to upload the CodeQL Action and
4243

4344
**Required Arguments:**
4445
* `--destination-url` - The URL of the GitHub Enterprise Server instance to push the Action to.
45-
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.
46+
* `--destination-token` - A [Personal Access Token](https://docs.github.com/en/enterprise/user/github/authenticating-to-github/creating-a-personal-access-token) for the destination GitHub Enterprise Server instance. The token should be granted at least the `public_repo` scope. If the destination repository is in an organization that does not yet exist or that you are not an owner of, your token will need to have the `site_admin` scope in order to create the organization. The organization can also be created manually or an existing organization used.
4647

4748
**Optional Arguments:**
4849
* `--cache-dir` - The directory to which the Action was previously downloaded.
4950
* `--destination-repository` - The name of the repository in which to create or update the CodeQL Action. If not specified `github/codeql-action` will be used.
51+
* `--actions-admin-user` - The name of the Actions admin user, which will be used if you are updating the bundled CodeQL Action. If not specified `actions-admin` will be used.
5052
* `--force` - By default the tool will not overwrite existing repositories. Providing this flag will allow it to.
5153
* `--push-ssh` - Push Git contents over SSH rather than HTTPS. To use this option you must have SSH access to your GitHub Enterprise instance configured.
5254

cmd/push.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ var pushCmd = &cobra.Command{
1313
RunE: func(cmd *cobra.Command, args []string) error {
1414
version.LogVersion()
1515
cacheDirectory := cachedirectory.NewCacheDirectory(rootFlags.cacheDir)
16-
return push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.force, pushFlags.pushSSH)
16+
return push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.actionsAdminUser, pushFlags.force, pushFlags.pushSSH)
1717
},
1818
}
1919

2020
type pushFlagFields struct {
2121
destinationURL string
2222
destinationToken string
2323
destinationRepository string
24+
actionsAdminUser string
2425
force bool
2526
pushSSH bool
2627
}
@@ -33,6 +34,7 @@ func (f *pushFlagFields) Init(cmd *cobra.Command) {
3334
cmd.Flags().StringVar(&f.destinationToken, "destination-token", "", "A token to access the API on the GitHub Enterprise instance.")
3435
cmd.MarkFlagRequired("destination-token")
3536
cmd.Flags().StringVar(&f.destinationRepository, "destination-repository", "github/codeql-action", "The name of the repository to create on GitHub Enterprise.")
37+
cmd.Flags().StringVar(&f.actionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user.")
3638
cmd.Flags().BoolVar(&f.force, "force", false, "Replace the existing repository even if it was not created by the sync tool.")
3739
cmd.Flags().BoolVar(&f.pushSSH, "push-ssh", false, "Push Git contents over SSH rather than HTTPS. To use this option you must have SSH access to your GitHub Enterprise instance configured.")
3840
}

cmd/sync.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var syncCmd = &cobra.Command{
1818
if err != nil {
1919
return err
2020
}
21-
err = push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.force, pushFlags.pushSSH)
21+
err = push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.actionsAdminUser, pushFlags.force, pushFlags.pushSSH)
2222
if err != nil {
2323
return err
2424
}

internal/push/push.go

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type pushService struct {
4343
githubEnterpriseClient *github.Client
4444
destinationRepositoryName string
4545
destinationRepositoryOwner string
46-
destinationToken string
46+
destinationToken *oauth2.Token
47+
actionsAdminUser string
4748
force bool
4849
pushSSH bool
4950
}
@@ -82,6 +83,19 @@ func (pushService *pushService) createRepository() (*github.Repository, error) {
8283
return nil, errors.Wrap(err, "Error creating organization.")
8384
}
8485
}
86+
87+
_, response, err = pushService.githubEnterpriseClient.Organizations.GetOrgMembership(pushService.ctx, user.GetLogin(), pushService.destinationRepositoryOwner)
88+
if err != nil && (response == nil || response.StatusCode != http.StatusNotFound) {
89+
return nil, errors.Wrap(err, "Failed to check membership of destination organization.")
90+
}
91+
if err != nil && githubapiutil.HasAnyScope(response, "site_admin") {
92+
log.Debugf("No access to destination organization. Switching to impersonation token for %s...", pushService.actionsAdminUser)
93+
impersonationToken, _, err := pushService.githubEnterpriseClient.Admin.CreateUserImpersonation(pushService.ctx, pushService.actionsAdminUser, &github.ImpersonateUserOptions{Scopes: []string{"public_repo", "workflow"}})
94+
if err != nil {
95+
return nil, errors.Wrap(err, "Failed to impersonate Actions admin user.")
96+
}
97+
pushService.destinationToken.AccessToken = impersonationToken.GetToken()
98+
}
8599
}
86100

87101
repository, response, err := pushService.githubEnterpriseClient.Repositories.Get(pushService.ctx, pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
@@ -111,28 +125,13 @@ func (pushService *pushService) createRepository() (*github.Repository, error) {
111125
return nil, errors.Wrap(err, "Error creating destination repository.")
112126
}
113127
} else {
114-
if githubapiutil.HasAnyScope(response, "site_admin") {
115-
impersonationToken, _, err := pushService.githubEnterpriseClient.Admin.CreateUserImpersonation(pushService.ctx, "actions-admin", &github.ImpersonateUserOptions{Scopes: []string{"repo"}})
116-
if err != nil {
117-
return nil, errors.Wrap(err, "Failed to impersonate Actions admin user.")
118-
}
119-
tokenSource := oauth2.StaticTokenSource(
120-
&oauth2.Token{AccessToken: impersonationToken.GetToken()},
121-
)
122-
tokenClient := oauth2.NewClient(pushService.ctx, tokenSource)
123-
pushService.destinationToken = impersonationToken.GetToken()
124-
pushService.githubEnterpriseClient, err = github.NewEnterpriseClient(pushService.githubEnterpriseClient.BaseURL.String(), pushService.githubEnterpriseClient.UploadURL.String(), tokenClient)
125-
if err != nil {
126-
return nil, errors.Wrap(err, "Error creating GitHub Enterprise client.")
127-
}
128-
}
129128
repository, response, err = pushService.githubEnterpriseClient.Repositories.Edit(pushService.ctx, pushService.destinationRepositoryOwner, pushService.destinationRepositoryName, &desiredRepositoryProperties)
130129
if err != nil {
131130
if response.StatusCode == http.StatusNotFound {
132131
if !githubapiutil.HasAnyScope(response, "public_repo", "repo") {
133132
return nil, usererrors.New("The destination token you have provided does not have the `public_repo` scope.")
134133
} else {
135-
return nil, fmt.Errorf("You don't have permission to update the repository at %s/%s.", pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
134+
return nil, fmt.Errorf("You don't have permission to update the repository at %s/%s. If you wish to update the bundled CodeQL Action please provide a token with the `site_admin` scope.", pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
136135
}
137136
}
138137
return nil, errors.Wrap(err, "Error updating destination repository.")
@@ -164,7 +163,7 @@ func (pushService *pushService) pushGit(repository *github.Repository, initialPu
164163

165164
credentials := &githttp.BasicAuth{
166165
Username: "x-access-token",
167-
Password: pushService.destinationToken,
166+
Password: pushService.destinationToken.AccessToken,
168167
}
169168
if pushService.pushSSH {
170169
// Use the SSH key from the environment.
@@ -346,7 +345,7 @@ func (pushService *pushService) pushReleases() error {
346345
return nil
347346
}
348347

349-
func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, destinationURL string, destinationToken string, destinationRepository string, force bool, pushSSH bool) error {
348+
func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, destinationURL string, destinationToken string, destinationRepository string, actionsAdminUser string, force bool, pushSSH bool) error {
350349
err := cacheDirectory.CheckOrCreateVersionFile(false, version.Version())
351350
if err != nil {
352351
return err
@@ -357,8 +356,9 @@ func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, des
357356
}
358357

359358
destinationURL = strings.TrimRight(destinationURL, "/")
359+
token := oauth2.Token{AccessToken: destinationToken}
360360
tokenSource := oauth2.StaticTokenSource(
361-
&oauth2.Token{AccessToken: destinationToken},
361+
&token,
362362
)
363363
tokenClient := oauth2.NewClient(ctx, tokenSource)
364364
client, err := github.NewEnterpriseClient(destinationURL+"/api/v3", destinationURL+"/api/uploads", tokenClient)
@@ -376,7 +376,8 @@ func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, des
376376
githubEnterpriseClient: client,
377377
destinationRepositoryOwner: destinationRepositoryOwner,
378378
destinationRepositoryName: destinationRepositoryName,
379-
destinationToken: destinationToken,
379+
destinationToken: &token,
380+
actionsAdminUser: actionsAdminUser,
380381
force: force,
381382
pushSSH: pushSSH,
382383
}

internal/push/push_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/go-git/go-git/v5"
1616
"github.com/gorilla/mux"
1717
"github.com/stretchr/testify/require"
18+
"golang.org/x/oauth2"
1819

1920
"github.com/google/go-github/v32/github"
2021
)
@@ -29,13 +30,14 @@ func getTestPushService(t *testing.T, cacheDirectoryString string, githubEnterpr
2930
} else {
3031
githubEnterpriseClient = nil
3132
}
33+
token := oauth2.Token{AccessToken: "token"}
3234
return pushService{
3335
ctx: context.Background(),
3436
cacheDirectory: cacheDirectory,
3537
githubEnterpriseClient: githubEnterpriseClient,
3638
destinationRepositoryOwner: "destination-repository-owner",
3739
destinationRepositoryName: "destination-repository-name",
38-
destinationToken: "token",
40+
destinationToken: &token,
3941
}
4042
}
4143

0 commit comments

Comments
 (0)