Skip to content

Commit

Permalink
Use an impersonation token to push to GitHub Enterprise if pushing di…
Browse files Browse the repository at this point in the history
…rectly is not allowed.

Co-authored-by: Simon Engledew <simon-engledew@github.com>
  • Loading branch information
chrisgavin and simon-engledew committed Nov 12, 2020
1 parent 993f67a commit ef02b83
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 26 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ From a machine with access to both GitHub.com and GitHub Enterprise Server use t

**Required Arguments:**
* `--destination-url` - The URL of the GitHub Enterprise Server instance to push the Action to.
* `--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.
* `--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.

**Optional Arguments:**
* `--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.
* `--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.
* `--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.
* `--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.
* `--force` - By default the tool will not overwrite existing repositories. Providing this flag will allow it to.
* `--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.

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

**Required Arguments:**
* `--destination-url` - The URL of the GitHub Enterprise Server instance to push the Action to.
* `--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.
* `--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.

**Optional Arguments:**
* `--cache-dir` - The directory to which the Action was previously downloaded.
* `--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.
* `--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.
* `--force` - By default the tool will not overwrite existing repositories. Providing this flag will allow it to.
* `--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.

Expand Down
4 changes: 3 additions & 1 deletion cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ var pushCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
version.LogVersion()
cacheDirectory := cachedirectory.NewCacheDirectory(rootFlags.cacheDir)
return push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.force, pushFlags.pushSSH)
return push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.actionsAdminUser, pushFlags.force, pushFlags.pushSSH)
},
}

type pushFlagFields struct {
destinationURL string
destinationToken string
destinationRepository string
actionsAdminUser string
force bool
pushSSH bool
}
Expand All @@ -33,6 +34,7 @@ func (f *pushFlagFields) Init(cmd *cobra.Command) {
cmd.Flags().StringVar(&f.destinationToken, "destination-token", "", "A token to access the API on the GitHub Enterprise instance.")
cmd.MarkFlagRequired("destination-token")
cmd.Flags().StringVar(&f.destinationRepository, "destination-repository", "github/codeql-action", "The name of the repository to create on GitHub Enterprise.")
cmd.Flags().StringVar(&f.actionsAdminUser, "actions-admin-user", "actions-admin", "The name of the Actions admin user.")
cmd.Flags().BoolVar(&f.force, "force", false, "Replace the existing repository even if it was not created by the sync tool.")
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.")
}
2 changes: 1 addition & 1 deletion cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var syncCmd = &cobra.Command{
if err != nil {
return err
}
err = push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.force, pushFlags.pushSSH)
err = push.Push(cmd.Context(), cacheDirectory, pushFlags.destinationURL, pushFlags.destinationToken, pushFlags.destinationRepository, pushFlags.actionsAdminUser, pushFlags.force, pushFlags.pushSSH)
if err != nil {
return err
}
Expand Down
43 changes: 22 additions & 21 deletions internal/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ type pushService struct {
githubEnterpriseClient *github.Client
destinationRepositoryName string
destinationRepositoryOwner string
destinationToken string
destinationToken *oauth2.Token
actionsAdminUser string
force bool
pushSSH bool
}
Expand Down Expand Up @@ -82,6 +83,19 @@ func (pushService *pushService) createRepository() (*github.Repository, error) {
return nil, errors.Wrap(err, "Error creating organization.")
}
}

_, response, err = pushService.githubEnterpriseClient.Organizations.GetOrgMembership(pushService.ctx, user.GetLogin(), pushService.destinationRepositoryOwner)
if err != nil && (response == nil || response.StatusCode != http.StatusNotFound) {
return nil, errors.Wrap(err, "Failed to check membership of destination organization.")
}
if err != nil && githubapiutil.HasAnyScope(response, "site_admin") {
log.Debugf("No access to destination organization. Switching to impersonation token for %s...", pushService.actionsAdminUser)
impersonationToken, _, err := pushService.githubEnterpriseClient.Admin.CreateUserImpersonation(pushService.ctx, pushService.actionsAdminUser, &github.ImpersonateUserOptions{Scopes: []string{"public_repo", "workflow"}})
if err != nil {
return nil, errors.Wrap(err, "Failed to impersonate Actions admin user.")
}
pushService.destinationToken.AccessToken = impersonationToken.GetToken()
}
}

repository, response, err := pushService.githubEnterpriseClient.Repositories.Get(pushService.ctx, pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
Expand Down Expand Up @@ -111,28 +125,13 @@ func (pushService *pushService) createRepository() (*github.Repository, error) {
return nil, errors.Wrap(err, "Error creating destination repository.")
}
} else {
if githubapiutil.HasAnyScope(response, "site_admin") {
impersonationToken, _, err := pushService.githubEnterpriseClient.Admin.CreateUserImpersonation(pushService.ctx, "actions-admin", &github.ImpersonateUserOptions{Scopes: []string{"repo"}})
if err != nil {
return nil, errors.Wrap(err, "Failed to impersonate Actions admin user.")
}
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: impersonationToken.GetToken()},
)
tokenClient := oauth2.NewClient(pushService.ctx, tokenSource)
pushService.destinationToken = impersonationToken.GetToken()
pushService.githubEnterpriseClient, err = github.NewEnterpriseClient(pushService.githubEnterpriseClient.BaseURL.String(), pushService.githubEnterpriseClient.UploadURL.String(), tokenClient)
if err != nil {
return nil, errors.Wrap(err, "Error creating GitHub Enterprise client.")
}
}
repository, response, err = pushService.githubEnterpriseClient.Repositories.Edit(pushService.ctx, pushService.destinationRepositoryOwner, pushService.destinationRepositoryName, &desiredRepositoryProperties)
if err != nil {
if response.StatusCode == http.StatusNotFound {
if !githubapiutil.HasAnyScope(response, "public_repo", "repo") {
return nil, usererrors.New("The destination token you have provided does not have the `public_repo` scope.")
} else {
return nil, fmt.Errorf("You don't have permission to update the repository at %s/%s.", pushService.destinationRepositoryOwner, pushService.destinationRepositoryName)
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)
}
}
return nil, errors.Wrap(err, "Error updating destination repository.")
Expand Down Expand Up @@ -164,7 +163,7 @@ func (pushService *pushService) pushGit(repository *github.Repository, initialPu

credentials := &githttp.BasicAuth{
Username: "x-access-token",
Password: pushService.destinationToken,
Password: pushService.destinationToken.AccessToken,
}
if pushService.pushSSH {
// Use the SSH key from the environment.
Expand Down Expand Up @@ -346,7 +345,7 @@ func (pushService *pushService) pushReleases() error {
return nil
}

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

destinationURL = strings.TrimRight(destinationURL, "/")
token := oauth2.Token{AccessToken: destinationToken}
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: destinationToken},
&token,
)
tokenClient := oauth2.NewClient(ctx, tokenSource)
client, err := github.NewEnterpriseClient(destinationURL+"/api/v3", destinationURL+"/api/uploads", tokenClient)
Expand All @@ -376,7 +376,8 @@ func Push(ctx context.Context, cacheDirectory cachedirectory.CacheDirectory, des
githubEnterpriseClient: client,
destinationRepositoryOwner: destinationRepositoryOwner,
destinationRepositoryName: destinationRepositoryName,
destinationToken: destinationToken,
destinationToken: &token,
actionsAdminUser: actionsAdminUser,
force: force,
pushSSH: pushSSH,
}
Expand Down
4 changes: 3 additions & 1 deletion internal/push/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/go-git/go-git/v5"
"github.com/gorilla/mux"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"

"github.com/google/go-github/v32/github"
)
Expand All @@ -29,13 +30,14 @@ func getTestPushService(t *testing.T, cacheDirectoryString string, githubEnterpr
} else {
githubEnterpriseClient = nil
}
token := oauth2.Token{AccessToken: "token"}
return pushService{
ctx: context.Background(),
cacheDirectory: cacheDirectory,
githubEnterpriseClient: githubEnterpriseClient,
destinationRepositoryOwner: "destination-repository-owner",
destinationRepositoryName: "destination-repository-name",
destinationToken: "token",
destinationToken: &token,
}
}

Expand Down

0 comments on commit ef02b83

Please sign in to comment.