diff --git a/github/client.go b/github/client.go index 068775d0..8c0c56fd 100644 --- a/github/client.go +++ b/github/client.go @@ -17,6 +17,9 @@ limitations under the License. package github import ( + "context" + "strings" + "github.com/google/go-github/v32/github" "github.com/fluxcd/go-git-providers/gitprovider" @@ -94,3 +97,35 @@ func (c *Client) OrgRepositories() gitprovider.OrgRepositoriesClient { func (c *Client) UserRepositories() gitprovider.UserRepositoriesClient { return c.userRepos } + +//nolint:gochecknoglobals +var permissionScopes = map[gitprovider.TokenPermission]string{ + gitprovider.TokenPermissionRWRepository: "repo", +} + +func (c *Client) HasTokenPermission(ctx context.Context, permission gitprovider.TokenPermission) (bool, error) { + requestedScope, ok := permissionScopes[permission] + if !ok { + return false, gitprovider.ErrNoProviderSupport + } + + // The X-OAuth-Scopes header is returned for any API calls, using Meta here to keep things simple. + _, res, err := c.c.Client().APIMeta(ctx) + if err != nil { + return false, err + } + + scopes := res.Header.Get("X-OAuth-Scopes") + if scopes == "" { + return false, gitprovider.ErrMissingHeader + } + + for _, s := range strings.Split(scopes, ",") { + scope := strings.TrimSpace(s) + if scope == requestedScope { + return true, nil + } + } + + return false, nil +} diff --git a/github/integration_test.go b/github/integration_test.go index a40743da..fd6b8b7a 100644 --- a/github/integration_test.go +++ b/github/integration_test.go @@ -248,8 +248,12 @@ var _ = Describe("GitHub Provider", func() { Expect(err).ToNot(HaveOccurred()) validateRepo(repo, repoRef) - getRepo, err := c.OrgRepositories().Get(ctx, repoRef) - Expect(err).ToNot(HaveOccurred()) + var getRepo gitprovider.OrgRepository + Eventually(func() error { + getRepo, err = c.OrgRepositories().Get(ctx, repoRef) + return err + }, 3*time.Second, 1*time.Second).ShouldNot(HaveOccurred()) + // Expect the two responses (one from POST and one from GET to have equal "spec") getSpec := newGithubRepositorySpec(getRepo.APIObject().(*github.Repository)) postSpec := newGithubRepositorySpec(repo.APIObject().(*github.Repository)) @@ -292,8 +296,12 @@ var _ = Describe("GitHub Provider", func() { _, err = anonClient.UserRepositories().Get(ctx, userRepoRef) Expect(errors.Is(err, gitprovider.ErrNotFound)).To(BeTrue()) - getUserRepo, err := c.UserRepositories().Get(ctx, userRepoRef) - Expect(err).ToNot(HaveOccurred()) + var getUserRepo gitprovider.UserRepository + Eventually(func() error { + getUserRepo, err = c.UserRepositories().Get(ctx, userRepoRef) + return err + }, 3*time.Second, 1*time.Second).ShouldNot(HaveOccurred()) + // Expect the two responses (one from POST and one from GET to have equal "spec") getSpec := newGithubRepositorySpec(getUserRepo.APIObject().(*github.Repository)) postSpec := newGithubRepositorySpec(userRepo.APIObject().(*github.Repository)) @@ -357,6 +365,16 @@ var _ = Describe("GitHub Provider", func() { Expect(actionTaken).To(BeTrue()) }) + It("should validate that the token has the correct permissions", func() { + hasPermission, err := c.HasTokenPermission(ctx, 0) + Expect(err).To(Equal(gitprovider.ErrNoProviderSupport)) + Expect(hasPermission).To(Equal(false)) + + hasPermission, err = c.HasTokenPermission(ctx, gitprovider.TokenPermissionRWRepository) + Expect(err).ToNot(HaveOccurred()) + Expect(hasPermission).To(Equal(true)) + }) + AfterSuite(func() { // Don't do anything more if c wasn't created if c == nil { diff --git a/gitlab/client.go b/gitlab/client.go index 852bae30..8822ca71 100644 --- a/gitlab/client.go +++ b/gitlab/client.go @@ -17,6 +17,8 @@ limitations under the License. package gitlab import ( + "context" + "github.com/fluxcd/go-git-providers/gitprovider" "github.com/xanzy/go-gitlab" ) @@ -102,3 +104,7 @@ func (c *Client) OrgRepositories() gitprovider.OrgRepositoriesClient { func (c *Client) UserRepositories() gitprovider.UserRepositoriesClient { return c.userRepos } + +func (c *Client) HasTokenPermission(ctx context.Context, permission gitprovider.TokenPermission) (bool, error) { + return false, gitprovider.ErrNoProviderSupport +} diff --git a/gitprovider/client.go b/gitprovider/client.go index 37d15d6d..ff596153 100644 --- a/gitprovider/client.go +++ b/gitprovider/client.go @@ -33,6 +33,10 @@ type Client interface { // This field is set at client creation time, and can't be changed. ProviderID() ProviderID + // HasTokenPermission returns a boolean indicating whether the supplied token has the requested + // permission. Permissions should be coarse-grained and applicable to *all* providers. + HasTokenPermission(ctx context.Context, permission TokenPermission) (bool, error) + // Raw returns the Go client used under the hood to access the Git provider. Raw() interface{} } diff --git a/gitprovider/enums.go b/gitprovider/enums.go index 639ac29c..ca80bc44 100644 --- a/gitprovider/enums.go +++ b/gitprovider/enums.go @@ -161,3 +161,10 @@ func ValidateLicenseTemplate(t LicenseTemplate) error { func LicenseTemplateVar(t LicenseTemplate) *LicenseTemplate { return &t } + +type TokenPermission int + +const ( + // Read/Write permission for public/private repositories. + TokenPermissionRWRepository TokenPermission = iota + 1 +) diff --git a/gitprovider/errors.go b/gitprovider/errors.go index 44cfb4b7..089dbfab 100644 --- a/gitprovider/errors.go +++ b/gitprovider/errors.go @@ -66,6 +66,8 @@ var ( // ErrInvalidPermissionLevel is the error returned when there is no mapping // from the given level to the gitprovider levels. ErrInvalidPermissionLevel = errors.New("invalid permission level") + // ErrMissingHeader is returned when an expected header is missing from the HTTP response. + ErrMissingHeader = errors.New("header is missing") ) // HTTPError is an error that contains context about the HTTP request/response that failed.