Skip to content

Commit 979c983

Browse files
authored
fix: [CLI-1118] Refactor rules of precedence for snyk auth (#416)
1 parent 049cd1c commit 979c983

File tree

3 files changed

+166
-17
lines changed

3 files changed

+166
-17
lines changed

pkg/app/app.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,32 @@ func defaultFuncApiUrl(_ configuration.Configuration, logger *zerolog.Logger) co
8383
urlString := constants.SNYK_DEFAULT_API_URL
8484
authToken := config.GetString(configuration.AUTHENTICATION_TOKEN)
8585

86+
// If a user specified their own value, start by respecting that
87+
if existingValue != nil {
88+
if temp, ok := existingValue.(string); ok {
89+
urlString = temp
90+
}
91+
}
92+
93+
// If an oauth token is provided, with a URL in the audience claim, use that instead
8694
urlFromOauthToken, err := auth.GetAudienceClaimFromOauthToken(config.GetString(auth.CONFIG_KEY_OAUTH_TOKEN))
8795
if err != nil {
8896
logger.Warn().Err(err).Msg("failed to read oauth token")
8997
}
9098

9199
if len(urlFromOauthToken) > 0 && len(urlFromOauthToken[0]) > 0 {
92100
urlString = urlFromOauthToken[0]
93-
} else if auth.IsAuthTypePAT(authToken) {
101+
}
102+
103+
// Same logic for PAT - if a PAT is provided, and it has a URL in the claims, use that instead
104+
if auth.IsAuthTypePAT(authToken) {
94105
apiUrl, claimsErr := auth.GetApiUrlFromPAT(authToken)
95106
if claimsErr != nil {
96107
logger.Warn().Err(claimsErr).Msg("failed to get api url from pat")
97108
}
98109
if len(apiUrl) > 0 {
99110
urlString = apiUrl
100111
}
101-
} else if existingValue != nil { // try the configured value as last resort
102-
if temp, ok := existingValue.(string); ok {
103-
urlString = temp
104-
}
105112
}
106113

107114
apiString, err := api.GetCanonicalApiUrlFromString(urlString)

pkg/app/app_test.go

Lines changed: 153 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import (
3434

3535
func Test_AddsDefaultFunctionForCustomConfigFiles(t *testing.T) {
3636
t.Run("should load default config files (without given command line)", func(t *testing.T) {
37-
engine := CreateAppEngine()
37+
localConfig := configuration.NewWithOpts()
38+
engine := CreateAppEngineWithOptions(WithConfiguration(localConfig))
3839
conf := engine.GetConfiguration()
3940

4041
actual := conf.GetStringSlice(configuration.CUSTOM_CONFIG_FILES)
@@ -50,7 +51,8 @@ func Test_AddsDefaultFunctionForCustomConfigFiles(t *testing.T) {
5051
})
5152

5253
t.Run("should load default config files (with given command line)", func(t *testing.T) {
53-
engine := CreateAppEngine()
54+
localConfig := configuration.NewWithOpts()
55+
engine := CreateAppEngineWithOptions(WithConfiguration(localConfig))
5456
conf := engine.GetConfiguration()
5557
conf.Set("configfile", "abc/d")
5658

@@ -98,12 +100,143 @@ func Test_CreateAppEngine_config_replaceV1inApi(t *testing.T) {
98100
assert.Equal(t, expectApiUrl, actualApiUrl)
99101
}
100102

103+
func Test_EnsureAuthConfigurationPrecedence(t *testing.T) {
104+
tests := []struct {
105+
name string
106+
patPayload string
107+
oauthJWTPayload string
108+
userDefinedApiUrl interface{}
109+
expectedURL string
110+
}{
111+
{
112+
name: "no user-specified input, should default to the hard-coded default URL",
113+
patPayload: "",
114+
oauthJWTPayload: "",
115+
userDefinedApiUrl: "",
116+
expectedURL: constants.SNYK_DEFAULT_API_URL,
117+
},
118+
{
119+
name: "broken user-defined API URL is defined, should default to the hard-coded default URL",
120+
patPayload: "",
121+
oauthJWTPayload: "",
122+
userDefinedApiUrl: 123,
123+
expectedURL: constants.SNYK_DEFAULT_API_URL,
124+
},
125+
{
126+
name: "only user-defined API URL is defined, use that",
127+
patPayload: "",
128+
oauthJWTPayload: "",
129+
userDefinedApiUrl: "https://api.user",
130+
expectedURL: "https://api.user",
131+
},
132+
{
133+
name: "with a broken PAT configured and a user-defined API URL, user-defined API URL should take precedence",
134+
patPayload: `{broken`,
135+
oauthJWTPayload: "",
136+
userDefinedApiUrl: "https://api.user",
137+
expectedURL: "https://api.user",
138+
},
139+
{
140+
name: "with an empty PAT configured and a user-defined API URL, user-defined API URL should take precedence",
141+
patPayload: `{}`,
142+
oauthJWTPayload: "",
143+
userDefinedApiUrl: "https://api.user",
144+
expectedURL: "https://api.user",
145+
},
146+
{
147+
name: "with a PAT configured and a user-defined API URL, PAT host should take precedence",
148+
patPayload: `{"h":"api.pat"}`,
149+
oauthJWTPayload: "",
150+
userDefinedApiUrl: "https://api.user",
151+
expectedURL: "https://api.pat",
152+
},
153+
{
154+
name: "with a broken OAuth with no host configured and a user-defined API URL, user-defined API URL should take precedence",
155+
patPayload: "",
156+
oauthJWTPayload: `{broken`,
157+
userDefinedApiUrl: "https://api.user",
158+
expectedURL: "https://api.user",
159+
},
160+
{
161+
name: "with OAuth with no host configured and a user-defined API URL, user-defined API URL should take precedence",
162+
patPayload: "",
163+
oauthJWTPayload: `{"sub":"1234567890","name":"John Doe","iat":1516239022,"aud":[]}`,
164+
userDefinedApiUrl: "https://api.user",
165+
expectedURL: "https://api.user",
166+
},
167+
{
168+
name: "with OAuth configured and a user-defined API URL, OAuth audience should take precedence",
169+
patPayload: "",
170+
oauthJWTPayload: `{"sub":"1234567890","name":"John Doe","iat":1516239022,"aud":["https://api.oauth"]}`,
171+
userDefinedApiUrl: "https://api.user",
172+
expectedURL: "https://api.oauth",
173+
},
174+
{
175+
name: "with only PAT configured, use PAT host",
176+
patPayload: `{"h":"api.pat"}`,
177+
oauthJWTPayload: "",
178+
userDefinedApiUrl: "",
179+
expectedURL: "https://api.pat",
180+
},
181+
{
182+
name: "with only OAuth configured, use OAuth audience",
183+
patPayload: "",
184+
oauthJWTPayload: `{"sub":"1234567890","name":"John Doe","iat":1516239022,"aud":["https://api.oauth"]}`,
185+
userDefinedApiUrl: "",
186+
expectedURL: "https://api.oauth",
187+
},
188+
// This is not a likely scenario, as you cannot define both at the same time. However, it will potentially
189+
// catch regressions if this test starts to fail.
190+
{
191+
name: "with PAT, OAuth and user-defined API URL, PAT should take precedence over OAuth",
192+
patPayload: `{"h":"api.pat"}`,
193+
oauthJWTPayload: `{"sub":"1234567890","name":"John Doe","iat":1516239022,"aud":["https://api.oauth"]}`,
194+
userDefinedApiUrl: "https://api.user",
195+
expectedURL: "https://api.pat",
196+
},
197+
}
198+
199+
for _, tt := range tests {
200+
t.Run(tt.name, func(t *testing.T) {
201+
config := configuration.NewWithOpts()
202+
engine := CreateAppEngineWithOptions(WithConfiguration(config))
203+
assert.NotNil(t, engine)
204+
205+
if tt.userDefinedApiUrl != "" {
206+
config.Set(configuration.API_URL, tt.userDefinedApiUrl)
207+
}
208+
209+
if tt.patPayload != "" {
210+
pat := createMockPAT(t, tt.patPayload)
211+
config.Set(configuration.AUTHENTICATION_TOKEN, pat)
212+
}
213+
214+
if tt.oauthJWTPayload != "" {
215+
jwtHeader := `{"alg":"HS256","typ":"JWT"}`
216+
encodedHeader := base64.RawURLEncoding.EncodeToString([]byte(jwtHeader))
217+
encodedPayload := base64.RawURLEncoding.EncodeToString([]byte(tt.oauthJWTPayload))
218+
signature := "hWq0fKukObQSkphAdyEC7-m4jXIb4VdWyQySmmgy0GU"
219+
220+
jwtToken := fmt.Sprintf("%s.%s.%s", encodedHeader, encodedPayload, signature)
221+
oauthTokenJSON := fmt.Sprintf(`{"access_token": "%s"}`, jwtToken)
222+
223+
config.Set(auth.CONFIG_KEY_OAUTH_TOKEN, oauthTokenJSON)
224+
}
225+
226+
actualApiUrl := config.GetString(configuration.API_URL)
227+
assert.Equal(t, tt.expectedURL, actualApiUrl)
228+
})
229+
}
230+
}
231+
101232
func Test_CreateAppEngine_config_PAT_autoRegionDetection(t *testing.T) {
102233
t.Run("default", func(t *testing.T) {
103234
apiUrl := "api.snyk.io"
104235
euPAT := createMockPAT(t, fmt.Sprintf(`{"h":"%s"}`, apiUrl))
105-
engine := CreateAppEngine()
106-
config := engine.GetConfiguration()
236+
config := configuration.NewWithOpts()
237+
engine := CreateAppEngineWithOptions(WithConfiguration(config))
238+
assert.NotNil(t, engine)
239+
107240
config.Set(configuration.AUTHENTICATION_TOKEN, euPAT)
108241

109242
actualApiUrl := config.GetString(configuration.API_URL)
@@ -113,8 +246,10 @@ func Test_CreateAppEngine_config_PAT_autoRegionDetection(t *testing.T) {
113246
t.Run("eu", func(t *testing.T) {
114247
apiUrl := "api.eu.snyk.io"
115248
euPAT := createMockPAT(t, fmt.Sprintf(`{"h":"%s"}`, apiUrl))
116-
engine := CreateAppEngine()
117-
config := engine.GetConfiguration()
249+
config := configuration.NewWithOpts()
250+
engine := CreateAppEngineWithOptions(WithConfiguration(config))
251+
assert.NotNil(t, engine)
252+
118253
config.Set(configuration.AUTHENTICATION_TOKEN, euPAT)
119254

120255
actualApiUrl := config.GetString(configuration.API_URL)
@@ -123,8 +258,10 @@ func Test_CreateAppEngine_config_PAT_autoRegionDetection(t *testing.T) {
123258

124259
t.Run("invalid PAT reverts to default API URL (with wrong payload)", func(t *testing.T) {
125260
patWithExtraSegments := "snyk_uat.12345678.payload.signature.extra"
126-
engine := CreateAppEngine()
127-
config := engine.GetConfiguration()
261+
config := configuration.NewWithOpts()
262+
engine := CreateAppEngineWithOptions(WithConfiguration(config))
263+
assert.NotNil(t, engine)
264+
128265
config.Set(configuration.AUTHENTICATION_TOKEN, patWithExtraSegments)
129266

130267
actualApiUrl := config.GetString(configuration.API_URL)
@@ -133,8 +270,10 @@ func Test_CreateAppEngine_config_PAT_autoRegionDetection(t *testing.T) {
133270

134271
t.Run("invalid PAT reverts to default API URL (with no hostname in claim)", func(t *testing.T) {
135272
pat := createMockPAT(t, `{}`)
136-
engine := CreateAppEngine()
137-
config := engine.GetConfiguration()
273+
config := configuration.NewWithOpts()
274+
engine := CreateAppEngineWithOptions(WithConfiguration(config))
275+
assert.NotNil(t, engine)
276+
138277
config.Set(configuration.AUTHENTICATION_TOKEN, pat)
139278

140279
actualApiUrl := config.GetString(configuration.API_URL)
@@ -609,7 +748,10 @@ func TestDefaultInputDirectory(t *testing.T) {
609748

610749
func Test_auth_oauth(t *testing.T) {
611750
mockCtl := gomock.NewController(t)
612-
engine := CreateAppEngine()
751+
config := configuration.NewWithOpts()
752+
engine := CreateAppEngineWithOptions(WithConfiguration(config))
753+
assert.NotNil(t, engine)
754+
613755
logger := engine.GetLogger()
614756
analytics := analytics.New()
615757

pkg/auth/oauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type arrayClaimSet struct {
1717
Aud []string `json:"aud"`
1818
}
1919

20-
// oauthApiUrl returns the API URL specified by the audience claim in a JWT
20+
// GetAudienceClaimFromOauthToken returns the API URL specified by the audience claim in a JWT
2121
// token established by a prior OAuth authentication flow.
2222
//
2323
// Returns an empty string if an OAuth token is not available, cannot be parsed,

0 commit comments

Comments
 (0)