Skip to content

Commit

Permalink
Merge pull request #53 from fluxcd/validate-token-permissions
Browse files Browse the repository at this point in the history
GitHub: Implement HasTokenPermission to ensure token has requested permission
GitLab: Return `ErrNoProviderSupport` as GitLab supports this only in certain versions
  • Loading branch information
yiannistri authored Sep 29, 2020
2 parents 65d81e5 + 17b7f5a commit a444520
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 4 deletions.
35 changes: 35 additions & 0 deletions github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
26 changes: 22 additions & 4 deletions github/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions gitlab/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package gitlab

import (
"context"

"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/xanzy/go-gitlab"
)
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions gitprovider/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
Expand Down
7 changes: 7 additions & 0 deletions gitprovider/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions gitprovider/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit a444520

Please sign in to comment.