Skip to content

Commit

Permalink
enhance: helper for installationCanReadRepo
Browse files Browse the repository at this point in the history
  • Loading branch information
plyr4 committed Oct 30, 2024
1 parent 9e0c98f commit ebf1786
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 59 deletions.
91 changes: 63 additions & 28 deletions scm/github/app_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package github
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptrace"
"net/url"
Expand All @@ -16,6 +17,7 @@ import (
"golang.org/x/oauth2"

api "github.com/go-vela/server/api/types"
"github.com/go-vela/server/constants"
)

// newOAuthTokenClient returns the GitHub OAuth client.
Expand Down Expand Up @@ -66,58 +68,91 @@ func (c *client) newGithubAppClient() (*github.Client, error) {
}

// newGithubAppInstallationRepoToken returns the GitHub App installation token for a particular repo with granular permissions.
func (c *client) newGithubAppInstallationRepoToken(ctx context.Context, r *api.Repo, repos []string, permissions *github.InstallationPermissions) (string, error) {
func (c *client) newGithubAppInstallationRepoToken(ctx context.Context, r *api.Repo, repos []string, permissions *github.InstallationPermissions) (*github.InstallationToken, int64, error) {
// create a github client based off the existing GitHub App configuration
client, err := c.newGithubAppClient()
if err != nil {
return "", err
return nil, 0, err
}

opts := &github.InstallationTokenOptions{
Repositories: repos,
Permissions: permissions,
}

id := r.GetInstallID()

// if repo has an install ID, use it to create an installation token
// todo: fix this: its fine but return the installation token object and
// update the repo when you can find a token for it...
if r.GetInstallID() != 0 {
// create installation token for the repo
t, _, err := client.Apps.CreateInstallationToken(ctx, r.GetInstallID(), opts)
if id == 0 {
// list all installations (a.k.a. orgs) where the GitHub App is installed
installations, _, err := client.Apps.ListInstallations(ctx, &github.ListOptions{})
if err != nil {
return "", err
return nil, 0, err
}

return t.GetToken(), nil
}

// list all installations (a.k.a. orgs) where the GitHub App is installed
installations, _, err := client.Apps.ListInstallations(ctx, &github.ListOptions{})
if err != nil {
return "", err
}

var id int64
// iterate through the list of installations
for _, install := range installations {
// find the installation that matches the org for the repo
if strings.EqualFold(install.GetAccount().GetLogin(), r.GetOrg()) {
// todo: right here... what if we're using this function to generate a netrc and the
// installation doesnt have access to that particular repo
id = install.GetID()
// iterate through the list of installations
for _, install := range installations {
// find the installation that matches the org for the repo
if strings.EqualFold(install.GetAccount().GetLogin(), r.GetOrg()) {
if install.GetRepositorySelection() == constants.AppInstallRepositoriesSelectionSelected {
installationCanReadRepo, err := c.installationCanReadRepo(ctx, r, install)
if err != nil {
return nil, 0, fmt.Errorf("installation for org %s exists but unable to check if it can read repo %s: %w", install.GetAccount().GetLogin(), r.GetFullName(), err)
}

if !installationCanReadRepo {
return nil, 0, fmt.Errorf("installation for org %s exists but does not have access to repo %s", install.GetAccount().GetLogin(), r.GetFullName())
}
}

id = install.GetID()
}
}
}

// failsafe in case the repo does not belong to an org where the GitHub App is installed
if id == 0 {
return "", errors.New("unable to find installation ID for repo")
return nil, 0, errors.New("unable to find installation ID for repo")
}

// create installation token for the repo
t, _, err := client.Apps.CreateInstallationToken(ctx, id, opts)
if err != nil {
return "", err
return nil, 0, err
}

return t, id, nil
}

// installationCanReadRepo checks if the installation can read the repo.
func (c *client) installationCanReadRepo(ctx context.Context, r *api.Repo, installation *github.Installation) (bool, error) {
installationCanReadRepo := false

if installation.GetRepositorySelection() == constants.AppInstallRepositoriesSelectionSelected {
client, err := c.newGithubAppClient()
if err != nil {
return false, err
}

t, _, err := client.Apps.CreateInstallationToken(ctx, installation.GetID(), &github.InstallationTokenOptions{})
if err != nil {
return false, err
}

client = c.newOAuthTokenClient(ctx, t.GetToken())

repos, _, err := client.Apps.ListRepos(ctx, &github.ListOptions{})
if err != nil {
return false, err
}

for _, repo := range repos.Repositories {
if strings.EqualFold(repo.GetFullName(), r.GetFullName()) {
installationCanReadRepo = true
break
}
}
}

return t.GetToken(), nil
return installationCanReadRepo, nil
}
48 changes: 17 additions & 31 deletions scm/github/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
api "github.com/go-vela/server/api/types"
"github.com/go-vela/server/compiler/types/yaml"
"github.com/go-vela/server/constants"
"github.com/go-vela/server/database"
)

// ConfigBackoff is a wrapper for Config that will retry five times if the function
Expand Down Expand Up @@ -733,10 +734,10 @@ func (c *client) GetNetrcPassword(ctx context.Context, r *api.Repo, u *api.User,
}
}

// the app might not be installed
// the app might not be installedm therefore we retain backwords compatibility via the user oauth token
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
// maybe take an optional list of repos and permission set that is driven by yaml
t, err := c.newGithubAppInstallationRepoToken(ctx, r, repos, ghPerms)
// the optional list of repos and permissions are driven by yaml
installToken, installID, err := c.newGithubAppInstallationRepoToken(ctx, r, repos, ghPerms)
if err != nil {
// return the legacy token along with no error for backwards compatibility
// todo: return an error based based on app installation requirements
Expand All @@ -745,10 +746,18 @@ func (c *client) GetNetrcPassword(ctx context.Context, r *api.Repo, u *api.User,
return u.GetToken(), nil
}

if len(t) != 0 {
if installToken != nil && len(installToken.GetToken()) != 0 {
l.Tracef("using github app installation token for %s/%s", r.GetOrg(), r.GetName())

return t, nil
// sync the install ID with the repo
r.SetInstallID(installID)

_, err = database.FromContext(ctx).UpdateRepo(ctx, r)
if err != nil {
c.Logger.Tracef("unable to update repo with install ID %d: %v", installID, err)
}

return installToken.GetToken(), nil
}

l.Tracef("using user oauth token for %s/%s", r.GetOrg(), r.GetName())
Expand Down Expand Up @@ -785,32 +794,9 @@ func (c *client) SyncRepoWithInstallation(ctx context.Context, r *api.Repo) (*ap
return nil, nil
}

installationCanReadRepo := false

if installation.GetRepositorySelection() != constants.AppInstallRepositoriesSelectionAll {
client, err := c.newGithubAppClient()
if err != nil {
return r, err
}

t, _, err := client.Apps.CreateInstallationToken(ctx, installation.GetID(), &github.InstallationTokenOptions{})
if err != nil {
return r, err
}

client = c.newOAuthTokenClient(ctx, t.GetToken())

repos, _, err := client.Apps.ListRepos(ctx, &github.ListOptions{})
if err != nil {
return r, err
}

for _, repo := range repos.Repositories {
if strings.EqualFold(repo.GetFullName(), r.GetFullName()) {
installationCanReadRepo = true
break
}
}
installationCanReadRepo, err := c.installationCanReadRepo(ctx, r, installation)
if err != nil {
return r, err
}

if installationCanReadRepo {
Expand Down

0 comments on commit ebf1786

Please sign in to comment.