Skip to content

Commit 30382d7

Browse files
CR-14127 - validate tokens (#570)
* validate tokens * wip * resolve comments * bump * wip * wip
1 parent cbfa58a commit 30382d7

File tree

10 files changed

+218
-43
lines changed

10 files changed

+218
-43
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION=v0.0.511
1+
VERSION=v0.0.512
22

33
OUT_DIR=dist
44
YEAR?=$(shell date +"%Y")

cmd/commands/common.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,15 @@ func ensureGitRuntimeToken(cmd *cobra.Command, gitProvider cfgit.Provider, clone
273273
}
274274

275275
if gitProvider != nil {
276-
err := gitProvider.VerifyRuntimeToken(ctx, cloneOpts.Auth.Password)
276+
err := gitProvider.VerifyRuntimeToken(ctx, cloneOpts.Auth)
277277
if err != nil {
278278
// in case when we get invalid value from env variable TOKEN we clean
279279
cloneOpts.Auth.Password = ""
280280
return fmt.Errorf(errMessage, err)
281281
}
282+
if cloneOpts.Auth.Username == "" && gitProvider.Type() == cfgit.BITBUCKET {
283+
return fmt.Errorf("must provide a git user using --git-user for bitbucket cloud")
284+
}
282285
} else if cloneOpts.Auth.Password == "" {
283286
return fmt.Errorf("must provide a git token using --git-token")
284287
}
@@ -303,7 +306,11 @@ func ensureGitUserToken(ctx context.Context, opts *RuntimeInstallOptions) error
303306
}
304307

305308
if opts.gitProvider != nil {
306-
return opts.gitProvider.VerifyUserToken(ctx, opts.GitIntegrationRegistrationOpts.Token)
309+
auth := apgit.Auth{
310+
Password: opts.GitIntegrationRegistrationOpts.Token,
311+
Username: opts.GitIntegrationRegistrationOpts.Username,
312+
}
313+
return opts.gitProvider.VerifyUserToken(ctx, auth)
307314
}
308315

309316
return nil

docs/releases/release_notes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ cf version
2323

2424
```bash
2525
# download and extract the binary
26-
curl -L --output - https://github.com/codefresh-io/cli-v2/releases/download/v0.0.511/cf-linux-amd64.tar.gz | tar zx
26+
curl -L --output - https://github.com/codefresh-io/cli-v2/releases/download/v0.0.512/cf-linux-amd64.tar.gz | tar zx
2727

2828
# move the binary to your $PATH
2929
mv ./cf-linux-amd64 /usr/local/bin/cf
@@ -36,7 +36,7 @@ cf version
3636

3737
```bash
3838
# download and extract the binary
39-
curl -L --output - https://github.com/codefresh-io/cli-v2/releases/download/v0.0.511/cf-darwin-amd64.tar.gz | tar zx
39+
curl -L --output - https://github.com/codefresh-io/cli-v2/releases/download/v0.0.512/cf-darwin-amd64.tar.gz | tar zx
4040

4141
# move the binary to your $PATH
4242
mv ./cf-darwin-amd64 /usr/local/bin/cf

manifests/runtime.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ metadata:
55
namespace: '{{ namespace }}'
66
spec:
77
defVersion: 2.0.0
8-
version: 0.0.511
8+
version: 0.0.512
99
bootstrapSpecifier: github.com/codefresh-io/cli-v2/manifests/argo-cd
1010
components:
1111
- name: events

pkg/git/provider.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"fmt"
2020
"net/http"
2121
"strings"
22+
23+
apgit "github.com/argoproj-labs/argocd-autopilot/pkg/git"
2224
)
2325

2426
//go:generate mockgen -destination=./mocks/roundTripper.go -package=mocks net/http RoundTripper
@@ -31,8 +33,8 @@ type (
3133
BaseURL() string
3234
SupportsMarketplace() bool
3335
Type() ProviderType
34-
VerifyRuntimeToken(ctx context.Context, token string) error
35-
VerifyUserToken(ctx context.Context, token string) error
36+
VerifyRuntimeToken(ctx context.Context, auth apgit.Auth) error
37+
VerifyUserToken(ctx context.Context, auth apgit.Auth) error
3638
}
3739
)
3840

pkg/git/provider_bitbucket-server.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"net/url"
2323
"path"
2424

25+
apgit "github.com/argoproj-labs/argocd-autopilot/pkg/git"
2526
httputil "github.com/codefresh-io/cli-v2/pkg/util/http"
2627
)
2728

@@ -71,12 +72,12 @@ func (bbs *bitbucketServer) Type() ProviderType {
7172
return bbs.providerType
7273
}
7374

74-
func (bbs *bitbucketServer) VerifyRuntimeToken(ctx context.Context, token string) error {
75-
return bbs.checkProjectAdminPermission(ctx, token)
75+
func (bbs *bitbucketServer) VerifyRuntimeToken(ctx context.Context, auth apgit.Auth) error {
76+
return bbs.checkProjectAdminPermission(ctx, auth.Password)
7677
}
7778

78-
func (bbs *bitbucketServer) VerifyUserToken(ctx context.Context, token string) error {
79-
return bbs.checkRepoReadPermission(ctx, token)
79+
func (bbs *bitbucketServer) VerifyUserToken(ctx context.Context, auth apgit.Auth) error {
80+
return bbs.checkRepoReadPermission(ctx, auth.Password)
8081
}
8182

8283
// POST to users/<username>/repos with an invalid repo name (starts with "!").

pkg/git/provider_bitbucket.go

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@ package git
1616

1717
import (
1818
"context"
19+
"errors"
20+
"fmt"
1921
"net/http"
2022
"net/url"
23+
"path"
24+
"strings"
25+
26+
"encoding/base64"
27+
28+
apgit "github.com/argoproj-labs/argocd-autopilot/pkg/git"
29+
httputil "github.com/codefresh-io/cli-v2/pkg/util/http"
2130
)
2231

2332
type (
@@ -30,10 +39,25 @@ type (
3039

3140
const (
3241
BITBUCKET_CLOUD_DOMAIN = "bitbucket.org"
33-
BITBUCKET_REST_ENDPOINT = "/2.0"
42+
BITBUCKET_REST_ENDPOINT = "/api/2.0"
3443
BITBUCKET ProviderType = "bitbucket"
3544
)
3645

46+
var (
47+
patScopes = [][]string{
48+
{"repository:admin", "repository:write"},
49+
{"account:read", "account:write"},
50+
{"team", "team:write"},
51+
}
52+
53+
runtimeScopes = [][]string{
54+
{"repository:admin"},
55+
{"account:read", "account:write"},
56+
{"team", "team:write"},
57+
{"webhook"},
58+
}
59+
)
60+
3761
func NewBitbucketProvider(baseURL string, client *http.Client) (Provider, error) {
3862
u, err := url.Parse(baseURL)
3963
if err != nil {
@@ -63,34 +87,70 @@ func (bb *bitbucket) Type() ProviderType {
6387
return bb.providerType
6488
}
6589

66-
func (bb *bitbucket) VerifyRuntimeToken(ctx context.Context, token string) error {
67-
return bb.checkProjectAdminPermission(ctx, token)
90+
func (bb *bitbucket) VerifyRuntimeToken(ctx context.Context, auth apgit.Auth) error {
91+
if auth.Password == "" {
92+
return fmt.Errorf("user name is require for bitbucket cloud request")
93+
}
94+
95+
return bb.verifyToken(ctx, auth.Password, auth.Username, runtimeScopes)
6896
}
6997

70-
func (bb *bitbucket) VerifyUserToken(ctx context.Context, token string) error {
71-
return bb.checkRepoReadPermission(ctx, token)
98+
func (bb *bitbucket) VerifyUserToken(ctx context.Context, auth apgit.Auth) error {
99+
if auth.Password == "" {
100+
return fmt.Errorf("user name is require for bitbucket cloud request")
101+
}
102+
return bb.verifyToken(ctx, auth.Password, auth.Username, patScopes)
72103
}
73104

74-
func (bb *bitbucket) checkProjectAdminPermission(ctx context.Context, token string) error {
105+
func (bb *bitbucket) verifyToken(ctx context.Context, token string, username string, requiredScopes [][]string) error {
106+
scopes, err := bb.getCurrentUserScopes(ctx, token, username)
107+
if err != nil {
108+
return fmt.Errorf("failed checking token scope permission: %w", err)
109+
}
110+
for _, requiredScope := range requiredScopes {
111+
isScopeIncluded := false
112+
for _, scopeOpt := range requiredScope {
113+
if strings.Contains(scopes, scopeOpt) {
114+
isScopeIncluded = true
115+
}
116+
}
117+
if !isScopeIncluded {
118+
return fmt.Errorf("the provided token is missing required token scopes, got: %s required: %v", scopes, requiredScopes)
119+
}
120+
}
121+
75122
return nil
76123
}
77124

78-
func (bb *bitbucket) checkRepoReadPermission(ctx context.Context, token string) error {
79-
return nil
125+
func (bb *bitbucket) getCurrentUserScopes(ctx context.Context, token, username string) (string, error) {
126+
res, err := bb.request(ctx, username, token, http.MethodHead, "user", nil)
127+
if err != nil {
128+
return "", fmt.Errorf("failed getting current user: %w", err)
129+
}
130+
defer res.Body.Close()
131+
132+
scopes := res.Header.Get("x-oauth-scopes")
133+
134+
if scopes == "" {
135+
return "", errors.New("invalid token")
136+
}
137+
138+
return scopes, nil
80139
}
81140

82-
// func (bb *bitbucket) request(ctx context.Context, token, method, urlPath string, body interface{}) (*http.Response, error) {
83-
// urlClone := *bb.apiURL
84-
// urlClone.Path = path.Join(urlClone.Path, urlPath)
85-
// headers := map[string]string{
86-
// "Authorization": "Bearer " + token,
87-
// "Accept": "application/json",
88-
// "Content-Type": "application/json",
89-
// }
90-
// req, err := httputil.NewRequest(ctx, method, urlClone.String(), headers, body)
91-
// if err != nil {
92-
// return nil, err
93-
// }
94-
95-
// return bb.c.Do(req)
96-
// }
141+
func (bb *bitbucket) request(ctx context.Context, username, token, method, urlPath string, body interface{}) (*http.Response, error) {
142+
urlClone := *bb.apiURL
143+
urlClone.Path = path.Join(urlClone.Path, urlPath)
144+
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token))
145+
headers := map[string]string{
146+
"Authorization": "Basic " + auth,
147+
"Accept": "application/json",
148+
"Content-Type": "application/json",
149+
}
150+
req, err := httputil.NewRequest(ctx, method, urlClone.String(), headers, body)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
return bb.c.Do(req)
156+
}

pkg/git/provider_bitbucket_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2022 The Codefresh Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package git
16+
17+
import (
18+
"context"
19+
"errors"
20+
"net/http"
21+
"testing"
22+
23+
"github.com/codefresh-io/cli-v2/pkg/git/mocks"
24+
"github.com/golang/mock/gomock"
25+
"github.com/stretchr/testify/assert"
26+
)
27+
28+
func newBitbucket(transport http.RoundTripper) *bitbucket {
29+
client := &http.Client{
30+
Transport: transport,
31+
}
32+
g, _ := NewBitbucketProvider("https://some.server", client)
33+
return g.(*bitbucket)
34+
}
35+
36+
func Test_bitbucket_verifyToken(t *testing.T) {
37+
tests := map[string]struct {
38+
requiredScopes [][]string
39+
wantErr string
40+
beforeFn func(t *testing.T, c *mocks.MockRoundTripper)
41+
}{
42+
"Should fail if HEAD fails": {
43+
wantErr: "failed checking token scope permission: failed getting current user: Head \"https://some.server/api/2.0/user\": some error",
44+
beforeFn: func(_ *testing.T, c *mocks.MockRoundTripper) {
45+
c.EXPECT().RoundTrip(gomock.AssignableToTypeOf(&http.Request{})).Return(nil, errors.New("some error"))
46+
},
47+
},
48+
"Should fail if no x-oauth-Scopes in res headers": {
49+
wantErr: "failed checking token scope permission: invalid token",
50+
beforeFn: func(_ *testing.T, c *mocks.MockRoundTripper) {
51+
header := http.Header{}
52+
c.EXPECT().RoundTrip(gomock.AssignableToTypeOf(&http.Request{})).Return(&http.Response{
53+
StatusCode: 200,
54+
Header: header,
55+
}, nil)
56+
},
57+
},
58+
"Should fail if required scope is not in res header": {
59+
requiredScopes: [][]string{{"scope 3"}},
60+
wantErr: "the provided token is missing required token scopes, got: scope 1, scope 2 required: [[scope 3]]",
61+
beforeFn: func(t *testing.T, c *mocks.MockRoundTripper) {
62+
c.EXPECT().RoundTrip(gomock.AssignableToTypeOf(&http.Request{})).Times(1).DoAndReturn(func(_ *http.Request) (*http.Response, error) {
63+
header := http.Header{}
64+
header.Add("X-Oauth-Scopes", "scope 1, scope 2")
65+
res := &http.Response{
66+
StatusCode: 200,
67+
Header: header,
68+
}
69+
return res, nil
70+
})
71+
},
72+
},
73+
"Should succeed if all required scopes or higher are in the res header": {
74+
requiredScopes: [][]string{
75+
{"scope 3:write", "scope 3:admin"},
76+
{"scope 4"},
77+
},
78+
beforeFn: func(t *testing.T, c *mocks.MockRoundTripper) {
79+
c.EXPECT().RoundTrip(gomock.AssignableToTypeOf(&http.Request{})).Times(1).DoAndReturn(func(_ *http.Request) (*http.Response, error) {
80+
header := http.Header{}
81+
header.Add("X-Oauth-Scopes", "scope 1, scope 2, scope 3:write, scope 4")
82+
res := &http.Response{
83+
StatusCode: 200,
84+
Header: header,
85+
}
86+
return res, nil
87+
})
88+
},
89+
},
90+
}
91+
for name, tt := range tests {
92+
t.Run(name, func(t *testing.T) {
93+
ctrl := gomock.NewController(t)
94+
mockTransport := mocks.NewMockRoundTripper(ctrl)
95+
tt.beforeFn(t, mockTransport)
96+
g := newBitbucket(mockTransport)
97+
err := g.verifyToken(context.Background(), "token", "username", tt.requiredScopes)
98+
if err != nil || tt.wantErr != "" {
99+
assert.EqualError(t, err, tt.wantErr)
100+
}
101+
})
102+
}
103+
}

pkg/git/provider_github.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"net/url"
2323
"strings"
2424

25+
apgit "github.com/argoproj-labs/argocd-autopilot/pkg/git"
2526
httputil "github.com/codefresh-io/cli-v2/pkg/util/http"
2627
)
2728

@@ -83,17 +84,17 @@ func (g *github) Type() ProviderType {
8384
return g.providerType
8485
}
8586

86-
func (g *github) VerifyRuntimeToken(ctx context.Context, token string) error {
87-
err := g.verifyToken(ctx, token, runtime_token_scopes)
87+
func (g *github) VerifyRuntimeToken(ctx context.Context, auth apgit.Auth) error {
88+
err := g.verifyToken(ctx, auth.Password, runtime_token_scopes)
8889
if err != nil {
8990
return fmt.Errorf("git-token invalid: %w", err)
9091
}
9192

9293
return nil
9394
}
9495

95-
func (g *github) VerifyUserToken(ctx context.Context, token string) error {
96-
err := g.verifyToken(ctx, token, user_token_scopes)
96+
func (g *github) VerifyUserToken(ctx context.Context, auth apgit.Auth) error {
97+
err := g.verifyToken(ctx, auth.Password, user_token_scopes)
9798
if err != nil {
9899
return fmt.Errorf("personal-git-token invalid: %w", err)
99100
}

0 commit comments

Comments
 (0)