Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
6646c1a
add oidc
EyalDelarea Mar 24, 2025
e84f8f7
change values
EyalDelarea Mar 24, 2025
f9f86ad
t
EyalDelarea Mar 24, 2025
8462a0b
update
EyalDelarea Mar 24, 2025
26f774c
update
EyalDelarea Mar 24, 2025
971b3e5
update
EyalDelarea Mar 24, 2025
594f3a6
improve
EyalDelarea Mar 26, 2025
1a3a0b9
update
EyalDelarea Mar 26, 2025
1c94cc1
update
EyalDelarea Mar 27, 2025
9cc12cc
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli-core into ad…
EyalDelarea Mar 27, 2025
63676db
refactor
EyalDelarea Mar 27, 2025
8c4fc2c
remove hardcoded values
EyalDelarea Mar 27, 2025
d527baa
CR
EyalDelarea Mar 27, 2025
a18985b
Add some tests
EyalDelarea Mar 27, 2025
d5ac941
Fix static analysis
EyalDelarea Mar 27, 2025
063b4e6
Fix static analysis
EyalDelarea Mar 27, 2025
4bed266
fix static check
EyalDelarea Mar 27, 2025
2e32a09
add jfrog ignore for test
EyalDelarea Mar 27, 2025
df40e3d
more jfrog-ignores
EyalDelarea Mar 27, 2025
8c2fc41
more jfrog-ignores
EyalDelarea Mar 27, 2025
41e2c2d
init params to avoid nil pointers
EyalDelarea Mar 27, 2025
0bbaa19
Update access token after exchange to fix usage reports
EyalDelarea Mar 27, 2025
b5f34f6
Test output
EyalDelarea Mar 30, 2025
9ad0bd2
update
EyalDelarea Mar 30, 2025
167d135
update
EyalDelarea Mar 30, 2025
4f96dd6
fix
EyalDelarea Mar 30, 2025
0a34cdf
test output
EyalDelarea Mar 30, 2025
eb3dd30
test
EyalDelarea Mar 30, 2025
9a6c18d
update
EyalDelarea Mar 30, 2025
dd6b877
export default value if needed
EyalDelarea Mar 30, 2025
b496712
Update client
EyalDelarea Mar 31, 2025
162a8df
dont mask output
EyalDelarea Mar 31, 2025
91ee2ad
dont set env
EyalDelarea Mar 31, 2025
db55255
Move oidc fields from config struct
EyalDelarea Mar 31, 2025
e5b0798
remove todo
EyalDelarea Mar 31, 2025
1e123e7
Fix NPE
EyalDelarea Mar 31, 2025
60723c4
Move oidc params to configCmd struct and not the configfile struct
EyalDelarea Mar 31, 2025
dc1405b
Rename function
EyalDelarea Mar 31, 2025
877ca1d
CR changes
EyalDelarea Mar 31, 2025
46e6fb0
Add application key reading utils
EyalDelarea Apr 1, 2025
1e897b4
rename oidc config params to be more specific & update usage params
EyalDelarea Apr 1, 2025
6be1897
rename
EyalDelarea Apr 1, 2025
6861005
Pull dev
EyalDelarea Apr 3, 2025
7217a02
Remove logs
EyalDelarea Apr 3, 2025
05ca182
Update Usage
EyalDelarea Apr 3, 2025
367a139
Rename function and log token debug details
EyalDelarea Apr 6, 2025
3f2998d
print OIDC debug logs
EyalDelarea Apr 6, 2025
a13d53c
Remove debug logs
EyalDelarea Apr 6, 2025
7d81bc0
Update client
EyalDelarea Apr 6, 2025
7a16304
Update to jfrog dev
EyalDelarea Apr 6, 2025
5982efd
Update consts
EyalDelarea Apr 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions common/cliutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package cliutils
import (
"errors"
"fmt"
"github.com/jfrog/jfrog-cli-core/v2/common/project"
"io"
"os"
"path/filepath"
"strconv"
"strings"

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

const (
JfConfigDirName = ".jfrog"
JfConfigFileName = "config.yml"
ApplicationRootYML = "application"
Key = "key"
)

func FixWinPathBySource(path string, fromSpec bool) string {
if strings.Count(path, "/") > 0 {
// Assuming forward slashes - not doubling backslash to allow regexp escaping
Expand Down Expand Up @@ -251,3 +260,39 @@ func FixWinPathsForFileSystemSourcedCmds(uploadSpec *spec.SpecFiles, specFlag, e
}
}
}

// Retrieves the application key from the .jfrog/config file or the environment variable.
// If the application key is not found in either, returns an empty string.
func ReadJFrogApplicationKeyFromConfigOrEnv() (applicationKeyValue string) {
applicationKeyValue = getApplicationKeyFromConfig()
if applicationKeyValue != "" {
log.Debug("Found application key in config file:", applicationKeyValue)
return
}
applicationKeyValue = os.Getenv(coreutils.ApplicationKey)
if applicationKeyValue != "" {
log.Debug("Found application key in environment variable:", applicationKeyValue)
return
}
log.Debug("Application key is not found in the config file or environment variable.")
return ""
}

func getApplicationKeyFromConfig() string {
configFilePath := filepath.Join(JfConfigDirName, JfConfigFileName)
vConfig, err := project.ReadConfigFile(configFilePath, project.YAML)
if err != nil {
log.Debug("error reading config file: %v", err)
return ""
}

application := vConfig.GetStringMapString(ApplicationRootYML)
applicationKey, ok := application[Key]
if !ok {
log.Debug("Application key is not found in the config file.")
return ""
}

log.Debug("Found application key:", applicationKey)
return applicationKey
}
78 changes: 78 additions & 0 deletions common/cliutils/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package cliutils

import (
testUtils "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"os"
"path/filepath"
"testing"

"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/stretchr/testify/assert"
)

func TestReadJFrogApplicationKeyFromConfigOrEnv(t *testing.T) {
configFilePath := filepath.Join(JfConfigDirName, JfConfigFileName)

// Test cases
tests := []struct {
name string
configContent string
envValue string
expectedResult string
}{
{
name: "Application key in config file",
configContent: "application:\n key: configKey",
envValue: "",
expectedResult: "configKey",
},
{
name: "Application key in environment variable",
configContent: "",
envValue: "envKey",
expectedResult: "envKey",
},
{
name: "Application key in both config file and environment variable",
configContent: "application:\n key: configKey",
envValue: "envKey",
expectedResult: "configKey",
},
{
name: "No application key in config file or environment variable",
configContent: "",
envValue: "",
expectedResult: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup temp dir for each test
testDirPath, err := filepath.Abs(filepath.Join("../", "tests", "applicationConfigTestDir"))
assert.NoError(t, err)
_, cleanUp := testUtils.CreateTestWorkspace(t, testDirPath)

// Write config content to file
if tt.configContent != "" {
err = os.WriteFile(configFilePath, []byte(tt.configContent), 0644)
assert.NoError(t, err)
}

// Set environment variable
if tt.envValue != "" {
assert.NoError(t, os.Setenv(coreutils.ApplicationKey, tt.envValue))
} else {
assert.NoError(t, os.Unsetenv(coreutils.ApplicationKey))
}

// Call the function
result := ReadJFrogApplicationKeyFromConfigOrEnv()

// Assert the result
assert.Equal(t, tt.expectedResult, result)
// delete temp folder
cleanUp()
})
}
}
11 changes: 11 additions & 0 deletions common/commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ func Exec(command Command) error {
return err
}

// ExecAndThenReportUsage runs the command and then triggers a usage report
// Is used for commands which don't have the full server details before execution
// For example: oidc exchange command, which will get access token only after execution.
func ExecAndThenReportUsage(cc Command) (err error) {
if err = cc.Run(); err != nil {
return
}
reportUsage(cc, nil)
return
}

func reportUsage(command Command, channel chan<- bool) {
// When the usage reporting is done, signal to the channel.
defer signalReportUsageFinished(channel)
Expand Down
93 changes: 71 additions & 22 deletions common/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"errors"
"fmt"
generic "github.com/jfrog/jfrog-cli-core/v2/general/token"
"net/url"
"os"
"reflect"
Expand Down Expand Up @@ -39,11 +40,11 @@ const (
BasicAuth AuthenticationMethod = "Username and Password / Reference token"
MTLS AuthenticationMethod = "Mutual TLS"
WebLogin AuthenticationMethod = "Web Login"
// Currently supported only non-interactively.
OIDC AuthenticationMethod = "OIDC"
)

const (
// Indicates that the config command uses OIDC authentication.
configOidcCommandName = "config_oidc"
// Default config command name.
configCommandName = "config"
)
Expand All @@ -63,8 +64,9 @@ type ConfigCommand struct {
// Forcibly make the configured server default.
makeDefault bool
// For unit tests
disablePrompts bool
cmdType ConfigAction
disablePrompts bool
cmdType ConfigAction
oidcSetupParams *generic.OidcParams
}

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

func (cc *ConfigCommand) CommandName() string {
oidcConfigured, err := clientUtils.GetBoolEnvValue(coreutils.UsageOidcConfigured, false)
if err != nil {
log.Warn("Failed to get the value of the environment variable: " + coreutils.UsageAutoPublishedBuild + ". " + err.Error())
}
if oidcConfigured {
return configOidcCommandName
}
return configCommandName
}

// ExecAndReportUsage runs the ConfigCommand and then triggers a usage report if needed,
// Report usage only if OIDC integration was used
// Usage must be sent after command execution as we need the server details to be set.
func (cc *ConfigCommand) ExecAndReportUsage() (err error) {
if err = cc.Run(); err != nil {
return
}
reportUsage(cc, nil)
return
}

func (cc *ConfigCommand) config() error {
configurations, err := cc.prepareConfigurationData()
if err != nil {
Expand Down Expand Up @@ -215,6 +199,12 @@ func (cc *ConfigCommand) getConfigurationNonInteractively() error {
}
}

if cc.OidcAuthMethodUsed() {
if err := exchangeOidcTokenAndSetAccessToken(cc); err != nil {
return err
}
}

if cc.details.AccessToken != "" && cc.details.User == "" {
if err := cc.validateTokenIsNotApiKey(); err != nil {
return err
Expand All @@ -224,6 +214,35 @@ func (cc *ConfigCommand) getConfigurationNonInteractively() error {
return nil
}

// When a user is configuration a new server with OIDC, we will exchange the token and set the access token.
func exchangeOidcTokenAndSetAccessToken(cc *ConfigCommand) error {
if err := validateOidcParams(cc.details.Url, cc.oidcSetupParams); err != nil {
return err
}
log.Debug("Exchanging OIDC token...")
exchangeOidcTokenCmd := generic.NewOidcTokenExchangeCommand()
exchangeOidcTokenCmd.
SetServerDetails(cc.details).
SetProviderName(cc.oidcSetupParams.ProviderName).
SetOidcTokenID(cc.oidcSetupParams.TokenId).
SetProviderType(cc.oidcSetupParams.ProviderType).
SetAudience(cc.oidcSetupParams.Audience).
SetApplicationKey(cc.oidcSetupParams.ApplicationKey).
SetProjectKey(cc.oidcSetupParams.ProjectKey).
SetRepository(cc.oidcSetupParams.Repository).
SetJobId(cc.oidcSetupParams.JobId).
SetRunId(cc.oidcSetupParams.RunId)

// Usage report will be sent only after execution in order to have valid token
err := ExecAndThenReportUsage(exchangeOidcTokenCmd)
if err != nil {
return err
}
// Update the config server details with the exchanged token
cc.details.AccessToken = exchangeOidcTokenCmd.GetExchangedToken()
return nil
}

func (cc *ConfigCommand) addTrailingSlashes() {
cc.details.ArtifactoryUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.ArtifactoryUrl)
cc.details.DistributionUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.DistributionUrl)
Expand Down Expand Up @@ -281,6 +300,7 @@ func (cc *ConfigCommand) prepareConfigurationData() ([]*config.ServerDetails, er
if cc.defaultDetails != nil {
cc.details.InsecureTls = cc.defaultDetails.InsecureTls
}
cc.oidcSetupParams = new(generic.OidcParams)
}

// Get configurations list
Expand Down Expand Up @@ -404,6 +424,7 @@ func (cc *ConfigCommand) promptAuthMethods() (selectedMethod AuthenticationMetho
WebLogin,
BasicAuth,
MTLS,
OIDC,
}
var selectableItems []ioutils.PromptItem
for _, curMethod := range authMethods {
Expand Down Expand Up @@ -483,6 +504,15 @@ func (cc *ConfigCommand) readClientCertInfoFromConsole() {
}
}

func (cc *ConfigCommand) SetOidcExchangeTokenId(id string) {
cc.oidcSetupParams.TokenId = id
}

// If OIDC params were provided it indicates that we should use OIDC authentication method.
func (cc *ConfigCommand) OidcAuthMethodUsed() bool {
return cc.oidcSetupParams != nil && cc.oidcSetupParams.ProviderName != ""
}

func readAccessTokenFromConsole(details *config.ServerDetails) error {
token, err := ioutils.ScanPasswordFromConsole("JFrog access token:")
if err == nil {
Expand Down Expand Up @@ -817,6 +847,11 @@ func (cc *ConfigCommand) handleWebLogin() error {
return nil
}

func (cc *ConfigCommand) SetOIDCParams(oidcDetails *generic.OidcParams) *ConfigCommand {
cc.oidcSetupParams = oidcDetails
return cc
}

// Return true if a URL is safe. URL is considered not safe if the following conditions are met:
// 1. The URL uses an http:// scheme
// 2. The URL leads to a URL outside the local machine
Expand Down Expand Up @@ -852,6 +887,7 @@ func assertSingleAuthMethod(details *config.ServerDetails) error {

type ConfigCommandConfiguration struct {
ServerDetails *config.ServerDetails
OidcParams *generic.OidcParams
Interactive bool
EncPassword bool
BasicAuthOnly bool
Expand All @@ -868,3 +904,16 @@ func GetAllServerIds() []string {
}
return serverIds
}

func validateOidcParams(platformUrl string, oidcParams *generic.OidcParams) error {
if platformUrl == "" {
return errorutils.CheckErrorf("the --url flag must be provided when --oidc-provider is used")
}
if oidcParams.TokenId == "" {
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.")
}
if oidcParams.ProviderName == "" {
return errorutils.CheckErrorf("the --oidc-provider flag must be provided when using OIDC authentication")
}
return nil
}
Loading
Loading