Skip to content

Commit edba164

Browse files
authored
Revert "Fix reference token privilege escalation (#1540)" (#1571)
1 parent 8d52a53 commit edba164

2 files changed

Lines changed: 44 additions & 220 deletions

File tree

utils/config/tokenrefresh.go

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ package config
22

33
import (
44
"errors"
5-
"fmt"
65
"sync"
76
"time"
87

98
"github.com/jfrog/jfrog-client-go/access"
109
accessservices "github.com/jfrog/jfrog-client-go/access/services"
11-
clientutils "github.com/jfrog/jfrog-client-go/utils"
1210
"github.com/jfrog/jfrog-client-go/utils/errorutils"
1311

1412
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
@@ -185,56 +183,23 @@ func writeNewTokens(serverConfiguration *ServerDetails, serverId, accessToken, r
185183
}
186184

187185
func createTokensForConfig(serverDetails *ServerDetails, expirySeconds int) (auth.CreateTokenResponseData, error) {
188-
expiresIn := uint(max(expirySeconds, 0)) // #nosec G115 -- expirySeconds is validated positive by callers
189-
createTokenParams := accessservices.CreateTokenParams{
190-
CommonTokenParams: auth.CommonTokenParams{
191-
Scope: "applied-permissions/user",
192-
ExpiresIn: &expiresIn,
193-
Refreshable: clientutils.Pointer(true),
194-
},
195-
Username: serverDetails.User,
196-
}
197-
198-
// First, try with the original credentials (basic auth: user + password).
199-
servicesManager, err := createAccessTokensServiceManager(serverDetails)
186+
servicesManager, err := createArtifactoryTokensServiceManager(serverDetails)
200187
if err != nil {
201188
return auth.CreateTokenResponseData{}, err
202189
}
203-
newToken, err := servicesManager.CreateAccessToken(createTokenParams)
204-
if err == nil {
205-
return newToken, nil
206-
}
207190

208-
// If basic auth failed and a password is available, retry using it as a Bearer token.
209-
// This handles reference tokens, which the Access service can resolve server-side.
210-
if serverDetails.Password != "" {
211-
bearerDetails := serverDetailsForBearerAuth(serverDetails, serverDetails.Password)
212-
servicesManager, err = createAccessTokensServiceManager(bearerDetails)
213-
if err != nil {
214-
return auth.CreateTokenResponseData{}, err
215-
}
216-
newToken, err = servicesManager.CreateAccessToken(createTokenParams)
217-
if err == nil {
218-
return newToken, nil
219-
}
220-
log.Debug("Access token creation with Bearer auth failed: " + err.Error())
221-
}
191+
createTokenParams := services.NewCreateTokenParams()
192+
createTokenParams.Username = serverDetails.User
193+
createTokenParams.ExpiresIn = expirySeconds
194+
// User-scoped token
195+
createTokenParams.Scope = "member-of-groups:*"
196+
createTokenParams.Refreshable = true
222197

223-
return auth.CreateTokenResponseData{}, fmt.Errorf(
224-
"automatic token creation via the Access API failed: %s. "+
225-
"If your JFrog Platform version does not support the Access API, please upgrade. "+
226-
"Alternatively, use the --basic-auth-only flag to skip automatic token creation",
227-
err.Error())
228-
}
229-
230-
// serverDetailsForBearerAuth returns a ServerDetails configured to authenticate using the
231-
// given token as a Bearer token, with user/password credentials cleared.
232-
func serverDetailsForBearerAuth(original *ServerDetails, token string) *ServerDetails {
233-
details := *original
234-
details.AccessToken = token
235-
details.User = ""
236-
details.Password = ""
237-
return &details
198+
newToken, err := servicesManager.CreateToken(createTokenParams)
199+
if err != nil {
200+
return auth.CreateTokenResponseData{}, err
201+
}
202+
return newToken, nil
238203
}
239204

240205
func CreateInitialRefreshableTokensIfNeeded(serverDetails *ServerDetails) (err error) {
@@ -256,10 +221,9 @@ func CreateInitialRefreshableTokensIfNeeded(serverDetails *ServerDetails) (err e
256221
return
257222
}
258223

259-
newToken, tokenErr := createTokensForConfig(serverDetails, serverDetails.ArtifactoryTokenRefreshInterval*60)
260-
if tokenErr != nil {
261-
serverDetails.ArtifactoryTokenRefreshInterval = 0
262-
return nil
224+
newToken, err := createTokensForConfig(serverDetails, serverDetails.ArtifactoryTokenRefreshInterval*60)
225+
if err != nil {
226+
return
263227
}
264228
// Remove initializing value.
265229
serverDetails.ArtifactoryTokenRefreshInterval = 0

utils/config/tokenrefresh_test.go

Lines changed: 29 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
package config
22

33
import (
4-
"encoding/json"
5-
"net/http"
6-
"net/http/httptest"
7-
"strings"
8-
"sync"
94
"testing"
105

116
configtests "github.com/jfrog/jfrog-cli-core/v2/utils/config/tests"
127
"github.com/stretchr/testify/assert"
13-
"github.com/stretchr/testify/require"
148
)
159

1610
func TestCreateInitialRefreshableTokensIfNeededEarlyReturns(t *testing.T) {
@@ -202,11 +196,21 @@ func TestCreateInitialRefreshableTokensIfNeededValidInputs(t *testing.T) {
202196

203197
err = CreateInitialRefreshableTokensIfNeeded(serverDetailsCopy)
204198

205-
// With the Access API migration, token creation fails gracefully when no server is reachable:
206-
// the function returns nil error and resets the interval to 0 (disabling token refresh).
207-
assert.NoError(t, err, "Should not return error — graceful fallback to basic auth")
208-
assert.Equal(t, 0, serverDetailsCopy.ArtifactoryTokenRefreshInterval,
209-
"ArtifactoryTokenRefreshInterval should be reset to 0 after token creation attempt")
199+
if tt.expectError {
200+
assert.Error(t, err, "Expected an error but got none")
201+
} else if err == nil {
202+
// Note: This will fail if createTokensForConfig requires actual Artifactory connection
203+
// In that case, this test would need to be an integration test with a mock server
204+
assert.Equal(t, tt.expectedIntervalAfterCall, serverDetailsCopy.ArtifactoryTokenRefreshInterval,
205+
"ArtifactoryTokenRefreshInterval should be reset to 0 after successful token creation")
206+
// Verify tokens were set (if no error occurred)
207+
// Note: This assumes createTokensForConfig succeeded
208+
// In a real scenario, you'd need to mock the Artifactory service
209+
if tt.shouldCreateTokens {
210+
assert.NotEmpty(t, serverDetailsCopy.AccessToken, "AccessToken should be set after successful creation")
211+
assert.NotEmpty(t, serverDetailsCopy.ArtifactoryRefreshToken, "ArtifactoryRefreshToken should be set after successful creation")
212+
}
213+
}
210214
})
211215
}
212216
}
@@ -302,11 +306,20 @@ func TestCreateInitialRefreshableTokensIfNeededInputValidation(t *testing.T) {
302306
// 1. Return early (if conditions are met)
303307
// 2. Attempt to create tokens and potentially fail due to invalid input
304308
// We validate that the function handles the input gracefully
305-
// With graceful fallback, token creation failures return nil error
306-
assert.NoError(t, err, "Should not return error — graceful fallback")
307-
if initialInterval > 0 {
308-
assert.Equal(t, 0, serverDetailsCopy.ArtifactoryTokenRefreshInterval,
309-
"Interval should be reset to 0 after token creation attempt")
309+
if err != nil {
310+
// If there's an error, it should be due to invalid configuration
311+
// The interval should be reset to 0 if token creation was attempted
312+
if serverDetailsCopy.ArtifactoryTokenRefreshInterval == 0 {
313+
// Token creation was attempted but failed
314+
assert.Error(t, err, tt.description)
315+
}
316+
} else {
317+
// If no error, either early return occurred or tokens were created successfully
318+
if initialInterval > 0 && serverDetailsCopy.ArtifactoryTokenRefreshInterval == 0 {
319+
// Tokens were created successfully
320+
assert.NotEmpty(t, serverDetailsCopy.AccessToken, "AccessToken should be set after successful creation")
321+
assert.NotEmpty(t, serverDetailsCopy.ArtifactoryRefreshToken, "ArtifactoryRefreshToken should be set after successful creation")
322+
}
310323
}
311324
})
312325
}
@@ -557,156 +570,3 @@ func TestCreateInitialRefreshableTokensIfNeededBranchCoverage(t *testing.T) {
557570
assert.NotNil(t, serverDetails)
558571
})
559572
}
560-
561-
func TestAccessAPITokenCreation_MockServer(t *testing.T) {
562-
cleanUpTempEnv := configtests.CreateTempEnv(t, false)
563-
defer cleanUpTempEnv()
564-
565-
var mu sync.Mutex
566-
var accessAPIHit bool
567-
var authMethod string
568-
569-
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
570-
if r.URL.Path == "/access/api/v1/tokens" && r.Method == http.MethodPost {
571-
mu.Lock()
572-
accessAPIHit = true
573-
if _, _, ok := r.BasicAuth(); ok {
574-
authMethod = "basic"
575-
} else if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
576-
authMethod = "bearer"
577-
}
578-
mu.Unlock()
579-
w.Header().Set("Content-Type", "application/json")
580-
// #nosec G101 -- mock test credentials, not real tokens
581-
resp := map[string]interface{}{
582-
"access_token": "mock-jwt-token", // #nosec G101
583-
"refresh_token": "mock-refresh-token", // #nosec G101
584-
"expires_in": 3600,
585-
"scope": "applied-permissions/user",
586-
"token_type": "Bearer",
587-
}
588-
_ = json.NewEncoder(w).Encode(resp)
589-
return
590-
}
591-
w.WriteHeader(http.StatusNotFound)
592-
}))
593-
defer mockServer.Close()
594-
595-
platformURL := mockServer.URL + "/"
596-
597-
serverDetails := &ServerDetails{
598-
ServerId: "test-access-api",
599-
ArtifactoryTokenRefreshInterval: 60,
600-
ArtifactoryRefreshToken: "",
601-
AccessToken: "",
602-
ArtifactoryUrl: mockServer.URL + "/artifactory/",
603-
Url: platformURL,
604-
User: "testuser",
605-
Password: "my-regular-password",
606-
}
607-
608-
err := SaveServersConf([]*ServerDetails{serverDetails})
609-
require.NoError(t, err)
610-
611-
_ = CreateInitialRefreshableTokensIfNeeded(serverDetails)
612-
613-
mu.Lock()
614-
assert.True(t, accessAPIHit,
615-
"Token creation should use the Access API endpoint")
616-
assert.Equal(t, "basic", authMethod,
617-
"Regular password should use basic auth")
618-
mu.Unlock()
619-
}
620-
621-
func TestBasicAuthFailsFallsBackToBearer_MockServer(t *testing.T) {
622-
cleanUpTempEnv := configtests.CreateTempEnv(t, false)
623-
defer cleanUpTempEnv()
624-
625-
var mu sync.Mutex
626-
var attempts []string
627-
628-
// #nosec G101 -- mock reference token, not a real credential
629-
fakeRefToken := "cmVmdGtuOjAxOjE3NzczNTczOTI6ZmFrZVRva2VuRm9yVGVzdGluZ09ubHk="
630-
631-
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
632-
if r.URL.Path == "/access/api/v1/tokens" && r.Method == http.MethodPost {
633-
mu.Lock()
634-
if _, _, ok := r.BasicAuth(); ok {
635-
attempts = append(attempts, "basic")
636-
mu.Unlock()
637-
w.WriteHeader(http.StatusUnauthorized)
638-
return
639-
} else if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
640-
attempts = append(attempts, "bearer")
641-
mu.Unlock()
642-
w.Header().Set("Content-Type", "application/json")
643-
// #nosec G101 -- mock test credentials, not real tokens
644-
resp := map[string]interface{}{
645-
"access_token": "mock-scoped-jwt", // #nosec G101
646-
"refresh_token": "mock-refresh-token", // #nosec G101
647-
"expires_in": 3600,
648-
"scope": "applied-permissions/user",
649-
"token_type": "Bearer",
650-
}
651-
_ = json.NewEncoder(w).Encode(resp)
652-
return
653-
}
654-
mu.Unlock()
655-
}
656-
w.WriteHeader(http.StatusNotFound)
657-
}))
658-
defer mockServer.Close()
659-
660-
serverDetails := &ServerDetails{
661-
ServerId: "test-retry-bearer",
662-
ArtifactoryTokenRefreshInterval: 60,
663-
ArtifactoryRefreshToken: "",
664-
AccessToken: "",
665-
ArtifactoryUrl: mockServer.URL + "/artifactory/",
666-
Url: mockServer.URL + "/",
667-
User: "testuser",
668-
Password: fakeRefToken,
669-
}
670-
671-
err := SaveServersConf([]*ServerDetails{serverDetails})
672-
require.NoError(t, err)
673-
674-
err = CreateInitialRefreshableTokensIfNeeded(serverDetails)
675-
assert.NoError(t, err)
676-
677-
mu.Lock()
678-
assert.Equal(t, []string{"basic", "bearer"}, attempts,
679-
"Should first try basic auth, then fall back to bearer when basic fails")
680-
mu.Unlock()
681-
}
682-
683-
func TestAccessAPITokenCreationFails_MockServer(t *testing.T) {
684-
cleanUpTempEnv := configtests.CreateTempEnv(t, false)
685-
defer cleanUpTempEnv()
686-
687-
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
688-
w.WriteHeader(http.StatusUnauthorized)
689-
}))
690-
defer mockServer.Close()
691-
692-
serverDetails := &ServerDetails{
693-
ServerId: "test-api-fail",
694-
ArtifactoryTokenRefreshInterval: 60,
695-
ArtifactoryRefreshToken: "",
696-
AccessToken: "",
697-
ArtifactoryUrl: mockServer.URL + "/",
698-
Url: mockServer.URL + "/",
699-
User: "testuser",
700-
Password: "my-regular-password",
701-
}
702-
703-
err := SaveServersConf([]*ServerDetails{serverDetails})
704-
require.NoError(t, err)
705-
706-
err = CreateInitialRefreshableTokensIfNeeded(serverDetails)
707-
assert.NoError(t, err, "Should not return error when Access API fails — gracefully falls back to basic auth")
708-
assert.Equal(t, 0, serverDetails.ArtifactoryTokenRefreshInterval,
709-
"Token refresh should be disabled after Access API failure")
710-
assert.Empty(t, serverDetails.AccessToken,
711-
"No access token should be set when Access API fails")
712-
}

0 commit comments

Comments
 (0)