Skip to content

Commit a444520

Browse files
authored
Merge pull request #53 from fluxcd/validate-token-permissions
GitHub: Implement HasTokenPermission to ensure token has requested permission GitLab: Return `ErrNoProviderSupport` as GitLab supports this only in certain versions
2 parents 65d81e5 + 17b7f5a commit a444520

File tree

6 files changed

+76
-4
lines changed

6 files changed

+76
-4
lines changed

github/client.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ limitations under the License.
1717
package github
1818

1919
import (
20+
"context"
21+
"strings"
22+
2023
"github.com/google/go-github/v32/github"
2124

2225
"github.com/fluxcd/go-git-providers/gitprovider"
@@ -94,3 +97,35 @@ func (c *Client) OrgRepositories() gitprovider.OrgRepositoriesClient {
9497
func (c *Client) UserRepositories() gitprovider.UserRepositoriesClient {
9598
return c.userRepos
9699
}
100+
101+
//nolint:gochecknoglobals
102+
var permissionScopes = map[gitprovider.TokenPermission]string{
103+
gitprovider.TokenPermissionRWRepository: "repo",
104+
}
105+
106+
func (c *Client) HasTokenPermission(ctx context.Context, permission gitprovider.TokenPermission) (bool, error) {
107+
requestedScope, ok := permissionScopes[permission]
108+
if !ok {
109+
return false, gitprovider.ErrNoProviderSupport
110+
}
111+
112+
// The X-OAuth-Scopes header is returned for any API calls, using Meta here to keep things simple.
113+
_, res, err := c.c.Client().APIMeta(ctx)
114+
if err != nil {
115+
return false, err
116+
}
117+
118+
scopes := res.Header.Get("X-OAuth-Scopes")
119+
if scopes == "" {
120+
return false, gitprovider.ErrMissingHeader
121+
}
122+
123+
for _, s := range strings.Split(scopes, ",") {
124+
scope := strings.TrimSpace(s)
125+
if scope == requestedScope {
126+
return true, nil
127+
}
128+
}
129+
130+
return false, nil
131+
}

github/integration_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,12 @@ var _ = Describe("GitHub Provider", func() {
248248
Expect(err).ToNot(HaveOccurred())
249249
validateRepo(repo, repoRef)
250250

251-
getRepo, err := c.OrgRepositories().Get(ctx, repoRef)
252-
Expect(err).ToNot(HaveOccurred())
251+
var getRepo gitprovider.OrgRepository
252+
Eventually(func() error {
253+
getRepo, err = c.OrgRepositories().Get(ctx, repoRef)
254+
return err
255+
}, 3*time.Second, 1*time.Second).ShouldNot(HaveOccurred())
256+
253257
// Expect the two responses (one from POST and one from GET to have equal "spec")
254258
getSpec := newGithubRepositorySpec(getRepo.APIObject().(*github.Repository))
255259
postSpec := newGithubRepositorySpec(repo.APIObject().(*github.Repository))
@@ -292,8 +296,12 @@ var _ = Describe("GitHub Provider", func() {
292296
_, err = anonClient.UserRepositories().Get(ctx, userRepoRef)
293297
Expect(errors.Is(err, gitprovider.ErrNotFound)).To(BeTrue())
294298

295-
getUserRepo, err := c.UserRepositories().Get(ctx, userRepoRef)
296-
Expect(err).ToNot(HaveOccurred())
299+
var getUserRepo gitprovider.UserRepository
300+
Eventually(func() error {
301+
getUserRepo, err = c.UserRepositories().Get(ctx, userRepoRef)
302+
return err
303+
}, 3*time.Second, 1*time.Second).ShouldNot(HaveOccurred())
304+
297305
// Expect the two responses (one from POST and one from GET to have equal "spec")
298306
getSpec := newGithubRepositorySpec(getUserRepo.APIObject().(*github.Repository))
299307
postSpec := newGithubRepositorySpec(userRepo.APIObject().(*github.Repository))
@@ -357,6 +365,16 @@ var _ = Describe("GitHub Provider", func() {
357365
Expect(actionTaken).To(BeTrue())
358366
})
359367

368+
It("should validate that the token has the correct permissions", func() {
369+
hasPermission, err := c.HasTokenPermission(ctx, 0)
370+
Expect(err).To(Equal(gitprovider.ErrNoProviderSupport))
371+
Expect(hasPermission).To(Equal(false))
372+
373+
hasPermission, err = c.HasTokenPermission(ctx, gitprovider.TokenPermissionRWRepository)
374+
Expect(err).ToNot(HaveOccurred())
375+
Expect(hasPermission).To(Equal(true))
376+
})
377+
360378
AfterSuite(func() {
361379
// Don't do anything more if c wasn't created
362380
if c == nil {

gitlab/client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package gitlab
1818

1919
import (
20+
"context"
21+
2022
"github.com/fluxcd/go-git-providers/gitprovider"
2123
"github.com/xanzy/go-gitlab"
2224
)
@@ -102,3 +104,7 @@ func (c *Client) OrgRepositories() gitprovider.OrgRepositoriesClient {
102104
func (c *Client) UserRepositories() gitprovider.UserRepositoriesClient {
103105
return c.userRepos
104106
}
107+
108+
func (c *Client) HasTokenPermission(ctx context.Context, permission gitprovider.TokenPermission) (bool, error) {
109+
return false, gitprovider.ErrNoProviderSupport
110+
}

gitprovider/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type Client interface {
3333
// This field is set at client creation time, and can't be changed.
3434
ProviderID() ProviderID
3535

36+
// HasTokenPermission returns a boolean indicating whether the supplied token has the requested
37+
// permission. Permissions should be coarse-grained and applicable to *all* providers.
38+
HasTokenPermission(ctx context.Context, permission TokenPermission) (bool, error)
39+
3640
// Raw returns the Go client used under the hood to access the Git provider.
3741
Raw() interface{}
3842
}

gitprovider/enums.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,10 @@ func ValidateLicenseTemplate(t LicenseTemplate) error {
161161
func LicenseTemplateVar(t LicenseTemplate) *LicenseTemplate {
162162
return &t
163163
}
164+
165+
type TokenPermission int
166+
167+
const (
168+
// Read/Write permission for public/private repositories.
169+
TokenPermissionRWRepository TokenPermission = iota + 1
170+
)

gitprovider/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ var (
6666
// ErrInvalidPermissionLevel is the error returned when there is no mapping
6767
// from the given level to the gitprovider levels.
6868
ErrInvalidPermissionLevel = errors.New("invalid permission level")
69+
// ErrMissingHeader is returned when an expected header is missing from the HTTP response.
70+
ErrMissingHeader = errors.New("header is missing")
6971
)
7072

7173
// HTTPError is an error that contains context about the HTTP request/response that failed.

0 commit comments

Comments
 (0)