Skip to content

Commit 7a3cb0b

Browse files
authored
Support OIDC Token Exchange During Server Configuration (#1369)
1 parent a562cbe commit 7a3cb0b

File tree

13 files changed

+494
-93
lines changed

13 files changed

+494
-93
lines changed

common/cliutils/utils.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package cliutils
33
import (
44
"errors"
55
"fmt"
6+
"github.com/jfrog/jfrog-cli-core/v2/common/project"
67
"io"
78
"os"
9+
"path/filepath"
810
"strconv"
911
"strings"
1012

@@ -18,6 +20,13 @@ import (
1820
"github.com/jfrog/jfrog-client-go/utils/log"
1921
)
2022

23+
const (
24+
JfConfigDirName = ".jfrog"
25+
JfConfigFileName = "config.yml"
26+
ApplicationRootYML = "application"
27+
Key = "key"
28+
)
29+
2130
func FixWinPathBySource(path string, fromSpec bool) string {
2231
if strings.Count(path, "/") > 0 {
2332
// Assuming forward slashes - not doubling backslash to allow regexp escaping
@@ -251,3 +260,39 @@ func FixWinPathsForFileSystemSourcedCmds(uploadSpec *spec.SpecFiles, specFlag, e
251260
}
252261
}
253262
}
263+
264+
// Retrieves the application key from the .jfrog/config file or the environment variable.
265+
// If the application key is not found in either, returns an empty string.
266+
func ReadJFrogApplicationKeyFromConfigOrEnv() (applicationKeyValue string) {
267+
applicationKeyValue = getApplicationKeyFromConfig()
268+
if applicationKeyValue != "" {
269+
log.Debug("Found application key in config file:", applicationKeyValue)
270+
return
271+
}
272+
applicationKeyValue = os.Getenv(coreutils.ApplicationKey)
273+
if applicationKeyValue != "" {
274+
log.Debug("Found application key in environment variable:", applicationKeyValue)
275+
return
276+
}
277+
log.Debug("Application key is not found in the config file or environment variable.")
278+
return ""
279+
}
280+
281+
func getApplicationKeyFromConfig() string {
282+
configFilePath := filepath.Join(JfConfigDirName, JfConfigFileName)
283+
vConfig, err := project.ReadConfigFile(configFilePath, project.YAML)
284+
if err != nil {
285+
log.Debug("error reading config file: %v", err)
286+
return ""
287+
}
288+
289+
application := vConfig.GetStringMapString(ApplicationRootYML)
290+
applicationKey, ok := application[Key]
291+
if !ok {
292+
log.Debug("Application key is not found in the config file.")
293+
return ""
294+
}
295+
296+
log.Debug("Found application key:", applicationKey)
297+
return applicationKey
298+
}

common/cliutils/utils_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cliutils
2+
3+
import (
4+
testUtils "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestReadJFrogApplicationKeyFromConfigOrEnv(t *testing.T) {
14+
configFilePath := filepath.Join(JfConfigDirName, JfConfigFileName)
15+
16+
// Test cases
17+
tests := []struct {
18+
name string
19+
configContent string
20+
envValue string
21+
expectedResult string
22+
}{
23+
{
24+
name: "Application key in config file",
25+
configContent: "application:\n key: configKey",
26+
envValue: "",
27+
expectedResult: "configKey",
28+
},
29+
{
30+
name: "Application key in environment variable",
31+
configContent: "",
32+
envValue: "envKey",
33+
expectedResult: "envKey",
34+
},
35+
{
36+
name: "Application key in both config file and environment variable",
37+
configContent: "application:\n key: configKey",
38+
envValue: "envKey",
39+
expectedResult: "configKey",
40+
},
41+
{
42+
name: "No application key in config file or environment variable",
43+
configContent: "",
44+
envValue: "",
45+
expectedResult: "",
46+
},
47+
}
48+
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
// Setup temp dir for each test
52+
testDirPath, err := filepath.Abs(filepath.Join("../", "tests", "applicationConfigTestDir"))
53+
assert.NoError(t, err)
54+
_, cleanUp := testUtils.CreateTestWorkspace(t, testDirPath)
55+
56+
// Write config content to file
57+
if tt.configContent != "" {
58+
err = os.WriteFile(configFilePath, []byte(tt.configContent), 0644)
59+
assert.NoError(t, err)
60+
}
61+
62+
// Set environment variable
63+
if tt.envValue != "" {
64+
assert.NoError(t, os.Setenv(coreutils.ApplicationKey, tt.envValue))
65+
} else {
66+
assert.NoError(t, os.Unsetenv(coreutils.ApplicationKey))
67+
}
68+
69+
// Call the function
70+
result := ReadJFrogApplicationKeyFromConfigOrEnv()
71+
72+
// Assert the result
73+
assert.Equal(t, tt.expectedResult, result)
74+
// delete temp folder
75+
cleanUp()
76+
})
77+
}
78+
}

common/commands/command.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ func Exec(command Command) error {
3939
return err
4040
}
4141

42+
// ExecAndThenReportUsage runs the command and then triggers a usage report
43+
// Is used for commands which don't have the full server details before execution
44+
// For example: oidc exchange command, which will get access token only after execution.
45+
func ExecAndThenReportUsage(cc Command) (err error) {
46+
if err = cc.Run(); err != nil {
47+
return
48+
}
49+
reportUsage(cc, nil)
50+
return
51+
}
52+
4253
func reportUsage(command Command, channel chan<- bool) {
4354
// When the usage reporting is done, signal to the channel.
4455
defer signalReportUsageFinished(channel)

common/commands/config.go

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"errors"
55
"fmt"
6+
generic "github.com/jfrog/jfrog-cli-core/v2/general/token"
67
"net/url"
78
"os"
89
"reflect"
@@ -39,11 +40,11 @@ const (
3940
BasicAuth AuthenticationMethod = "Username and Password / Reference token"
4041
MTLS AuthenticationMethod = "Mutual TLS"
4142
WebLogin AuthenticationMethod = "Web Login"
43+
// Currently supported only non-interactively.
44+
OIDC AuthenticationMethod = "OIDC"
4245
)
4346

4447
const (
45-
// Indicates that the config command uses OIDC authentication.
46-
configOidcCommandName = "config_oidc"
4748
// Default config command name.
4849
configCommandName = "config"
4950
)
@@ -63,8 +64,9 @@ type ConfigCommand struct {
6364
// Forcibly make the configured server default.
6465
makeDefault bool
6566
// For unit tests
66-
disablePrompts bool
67-
cmdType ConfigAction
67+
disablePrompts bool
68+
cmdType ConfigAction
69+
oidcSetupParams *generic.OidcParams
6870
}
6971

7072
func NewConfigCommand(cmdType ConfigAction, serverId string) *ConfigCommand {
@@ -150,27 +152,9 @@ func (cc *ConfigCommand) ServerDetails() (*config.ServerDetails, error) {
150152
}
151153

152154
func (cc *ConfigCommand) CommandName() string {
153-
oidcConfigured, err := clientUtils.GetBoolEnvValue(coreutils.UsageOidcConfigured, false)
154-
if err != nil {
155-
log.Warn("Failed to get the value of the environment variable: " + coreutils.UsageAutoPublishedBuild + ". " + err.Error())
156-
}
157-
if oidcConfigured {
158-
return configOidcCommandName
159-
}
160155
return configCommandName
161156
}
162157

163-
// ExecAndReportUsage runs the ConfigCommand and then triggers a usage report if needed,
164-
// Report usage only if OIDC integration was used
165-
// Usage must be sent after command execution as we need the server details to be set.
166-
func (cc *ConfigCommand) ExecAndReportUsage() (err error) {
167-
if err = cc.Run(); err != nil {
168-
return
169-
}
170-
reportUsage(cc, nil)
171-
return
172-
}
173-
174158
func (cc *ConfigCommand) config() error {
175159
configurations, err := cc.prepareConfigurationData()
176160
if err != nil {
@@ -215,6 +199,12 @@ func (cc *ConfigCommand) getConfigurationNonInteractively() error {
215199
}
216200
}
217201

202+
if cc.OidcAuthMethodUsed() {
203+
if err := exchangeOidcTokenAndSetAccessToken(cc); err != nil {
204+
return err
205+
}
206+
}
207+
218208
if cc.details.AccessToken != "" && cc.details.User == "" {
219209
if err := cc.validateTokenIsNotApiKey(); err != nil {
220210
return err
@@ -224,6 +214,35 @@ func (cc *ConfigCommand) getConfigurationNonInteractively() error {
224214
return nil
225215
}
226216

217+
// When a user is configuration a new server with OIDC, we will exchange the token and set the access token.
218+
func exchangeOidcTokenAndSetAccessToken(cc *ConfigCommand) error {
219+
if err := validateOidcParams(cc.details.Url, cc.oidcSetupParams); err != nil {
220+
return err
221+
}
222+
log.Debug("Exchanging OIDC token...")
223+
exchangeOidcTokenCmd := generic.NewOidcTokenExchangeCommand()
224+
exchangeOidcTokenCmd.
225+
SetServerDetails(cc.details).
226+
SetProviderName(cc.oidcSetupParams.ProviderName).
227+
SetOidcTokenID(cc.oidcSetupParams.TokenId).
228+
SetProviderType(cc.oidcSetupParams.ProviderType).
229+
SetAudience(cc.oidcSetupParams.Audience).
230+
SetApplicationKey(cc.oidcSetupParams.ApplicationKey).
231+
SetProjectKey(cc.oidcSetupParams.ProjectKey).
232+
SetRepository(cc.oidcSetupParams.Repository).
233+
SetJobId(cc.oidcSetupParams.JobId).
234+
SetRunId(cc.oidcSetupParams.RunId)
235+
236+
// Usage report will be sent only after execution in order to have valid token
237+
err := ExecAndThenReportUsage(exchangeOidcTokenCmd)
238+
if err != nil {
239+
return err
240+
}
241+
// Update the config server details with the exchanged token
242+
cc.details.AccessToken = exchangeOidcTokenCmd.GetExchangedToken()
243+
return nil
244+
}
245+
227246
func (cc *ConfigCommand) addTrailingSlashes() {
228247
cc.details.ArtifactoryUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.ArtifactoryUrl)
229248
cc.details.DistributionUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.DistributionUrl)
@@ -281,6 +300,7 @@ func (cc *ConfigCommand) prepareConfigurationData() ([]*config.ServerDetails, er
281300
if cc.defaultDetails != nil {
282301
cc.details.InsecureTls = cc.defaultDetails.InsecureTls
283302
}
303+
cc.oidcSetupParams = new(generic.OidcParams)
284304
}
285305

286306
// Get configurations list
@@ -404,6 +424,7 @@ func (cc *ConfigCommand) promptAuthMethods() (selectedMethod AuthenticationMetho
404424
WebLogin,
405425
BasicAuth,
406426
MTLS,
427+
OIDC,
407428
}
408429
var selectableItems []ioutils.PromptItem
409430
for _, curMethod := range authMethods {
@@ -483,6 +504,15 @@ func (cc *ConfigCommand) readClientCertInfoFromConsole() {
483504
}
484505
}
485506

507+
func (cc *ConfigCommand) SetOidcExchangeTokenId(id string) {
508+
cc.oidcSetupParams.TokenId = id
509+
}
510+
511+
// If OIDC params were provided it indicates that we should use OIDC authentication method.
512+
func (cc *ConfigCommand) OidcAuthMethodUsed() bool {
513+
return cc.oidcSetupParams != nil && cc.oidcSetupParams.ProviderName != ""
514+
}
515+
486516
func readAccessTokenFromConsole(details *config.ServerDetails) error {
487517
token, err := ioutils.ScanPasswordFromConsole("JFrog access token:")
488518
if err == nil {
@@ -817,6 +847,11 @@ func (cc *ConfigCommand) handleWebLogin() error {
817847
return nil
818848
}
819849

850+
func (cc *ConfigCommand) SetOIDCParams(oidcDetails *generic.OidcParams) *ConfigCommand {
851+
cc.oidcSetupParams = oidcDetails
852+
return cc
853+
}
854+
820855
// Return true if a URL is safe. URL is considered not safe if the following conditions are met:
821856
// 1. The URL uses an http:// scheme
822857
// 2. The URL leads to a URL outside the local machine
@@ -852,6 +887,7 @@ func assertSingleAuthMethod(details *config.ServerDetails) error {
852887

853888
type ConfigCommandConfiguration struct {
854889
ServerDetails *config.ServerDetails
890+
OidcParams *generic.OidcParams
855891
Interactive bool
856892
EncPassword bool
857893
BasicAuthOnly bool
@@ -868,3 +904,16 @@ func GetAllServerIds() []string {
868904
}
869905
return serverIds
870906
}
907+
908+
func validateOidcParams(platformUrl string, oidcParams *generic.OidcParams) error {
909+
if platformUrl == "" {
910+
return errorutils.CheckErrorf("the --url flag must be provided when --oidc-provider is used")
911+
}
912+
if oidcParams.TokenId == "" {
913+
return errorutils.CheckErrorf("the --oidc-token-id flag must be provided when --oidc-provider is used. Ensure the flag is set or the environment variable is exported. If running on a CI server, verify the token is correctly injected.")
914+
}
915+
if oidcParams.ProviderName == "" {
916+
return errorutils.CheckErrorf("the --oidc-provider flag must be provided when using OIDC authentication")
917+
}
918+
return nil
919+
}

0 commit comments

Comments
 (0)