github.com/jfrog/jfrog-cli-core/v2@v2.52.0/common/commands/config_test.go (about) 1 package commands 2 3 import ( 4 "encoding/json" 5 "os" 6 "testing" 7 8 "github.com/jfrog/jfrog-cli-core/v2/common/tests" 9 utilsTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" 10 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 11 12 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 13 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 14 "github.com/jfrog/jfrog-cli-core/v2/utils/log" 15 "github.com/stretchr/testify/assert" 16 ) 17 18 const ( 19 testServerId = "test" 20 // #nosec G101 -- False positive - no hardcoded credentials. 21 // jfrog-ignore - not a real token 22 acmeConfigToken = "eyJ2ZXJzaW9uIjoyLCJ1cmwiOiJodHRwczovL2FjbWUuamZyb2cuaW8vIiwiYXJ0aWZhY3RvcnlVcmwiOiJodHRwczovL2FjbWUuamZyb2cuaW8vYXJ0aWZhY3RvcnkvIiwiZGlzdHJpYnV0aW9uVXJsIjoiaHR0cHM6Ly9hY21lLmpmcm9nLmlvL2Rpc3RyaWJ1dGlvbi8iLCJ4cmF5VXJsIjoiaHR0cHM6Ly9hY21lLmpmcm9nLmlvL3hyYXkvIiwibWlzc2lvbkNvbnRyb2xVcmwiOiJodHRwczovL2FjbWUuamZyb2cuaW8vbWMvIiwicGlwZWxpbmVzVXJsIjoiaHR0cHM6Ly9hY21lLmpmcm9nLmlvL3BpcGVsaW5lcy8iLCJ1c2VyIjoiYWRtaW4iLCJwYXNzd29yZCI6InBhc3N3b3JkIiwidG9rZW5SZWZyZXNoSW50ZXJ2YWwiOjYwLCJzZXJ2ZXJJZCI6ImFjbWUifQ==" 23 ) 24 25 func init() { 26 log.SetDefaultLogger() 27 } 28 29 func TestBasicAuth(t *testing.T) { 30 inputDetails := tests.CreateTestServerDetails() 31 inputDetails.User = "admin" 32 inputDetails.Password = "password" 33 34 configAndTest(t, inputDetails, false) 35 configAndTest(t, inputDetails, true) 36 } 37 38 func TestUsernameSavedLowercase(t *testing.T) { 39 inputDetails := tests.CreateTestServerDetails() 40 inputDetails.User = "ADMIN" 41 inputDetails.Password = "password" 42 43 outputConfig, err := configAndGetTestServer(t, inputDetails, false, false) 44 assert.NoError(t, err) 45 assert.Equal(t, outputConfig.User, "admin", "The config command is supposed to save username as lowercase") 46 } 47 48 func TestDefaultServerId(t *testing.T) { 49 inputDetails := tests.CreateTestServerDetails() 50 inputDetails.User = "admin" 51 inputDetails.Password = "password" 52 // Remove server ID to verify the default one will be applied in non-interactive execution. 53 inputDetails.ServerId = "" 54 55 doConfig(t, "", inputDetails, false, true, false) 56 outputConfig, err := GetConfig(config.DefaultServerId, false) 57 assert.NoError(t, err) 58 assert.Equal(t, config.DefaultServerId, outputConfig.ServerId) 59 assert.NoError(t, NewConfigCommand(Delete, config.DefaultServerId).Run()) 60 } 61 62 func TestArtifactorySshKey(t *testing.T) { 63 inputDetails := tests.CreateTestServerDetails() 64 inputDetails.SshKeyPath = "/tmp/sshKey" 65 inputDetails.SshPassphrase = "123456" 66 inputDetails.ArtifactoryUrl = "ssh://localhost:1339/" 67 68 configAndTest(t, inputDetails, false) 69 configAndTest(t, inputDetails, true) 70 } 71 72 func TestAccessToken(t *testing.T) { 73 inputDetails := tests.CreateTestServerDetails() 74 inputDetails.AccessToken = "accessToken" 75 76 configAndTest(t, inputDetails, false) 77 configAndTest(t, inputDetails, true) 78 } 79 80 func TestAccessTokenWithUsername(t *testing.T) { 81 inputDetails := tests.CreateTestServerDetails() 82 inputDetails.AccessToken = "accessToken" 83 inputDetails.User = "ADMIN" 84 85 configAndTest(t, inputDetails, false) 86 configAndTest(t, inputDetails, true) 87 } 88 89 func TestApiKeyInAccessToken(t *testing.T) { 90 inputDetails := tests.CreateTestServerDetails() 91 apiKey := "AKCp8" + "fsafsadfkljaodjpioqwu4-32742398ujklwertjp89347583jtklsdfmgklsdjuftp397859jsdklfnsljgflkdsjlgjld" 92 inputDetails.AccessToken = apiKey 93 94 // Should throw error if access token is API key and no username 95 configCmd := NewConfigCommand(AddOrEdit, testServerId).SetDetails(inputDetails).SetUseBasicAuthOnly(true).SetInteractive(false) 96 configCmd.disablePrompts = true 97 assert.ErrorContains(t, configCmd.Run(), "the provided Access Token is an API key") 98 99 // Should work without error if access token is API key but username exists 100 inputDetails.User = "ADMIN" 101 configCmd.SetDetails(inputDetails) 102 assert.NoError(t, configCmd.Run()) 103 } 104 105 func TestMTLS(t *testing.T) { 106 inputDetails := tests.CreateTestServerDetails() 107 inputDetails.ClientCertPath = "test/cert/path" 108 inputDetails.ClientCertKeyPath = "test/cert/key/path" 109 110 configAndTest(t, inputDetails, false) 111 configAndTest(t, inputDetails, true) 112 } 113 114 func TestArtifactoryRefreshToken(t *testing.T) { 115 // Import after tokens were generated. 116 inputDetails := tests.CreateTestServerDetails() 117 inputDetails.User = "admin" 118 inputDetails.Password = "password" 119 inputDetails.AccessToken = "accessToken" 120 inputDetails.ArtifactoryRefreshToken = "refreshToken" 121 122 configAndTest(t, inputDetails, false) 123 configAndTest(t, inputDetails, true) 124 125 // Import before tokens were generated. 126 inputDetails.AccessToken = "" 127 inputDetails.ArtifactoryRefreshToken = "" 128 configAndTest(t, inputDetails, false) 129 configAndTest(t, inputDetails, true) 130 } 131 132 func TestEmptyCredentials(t *testing.T) { 133 configAndTest(t, tests.CreateTestServerDetails(), false) 134 } 135 136 func TestUrls(t *testing.T) { 137 t.Run("non-interactive", func(t *testing.T) { testUrls(t, false) }) 138 t.Run("interactive", func(t *testing.T) { testUrls(t, true) }) 139 } 140 141 func testUrls(t *testing.T, interactive bool) { 142 inputDetails := config.ServerDetails{ 143 Url: "http://localhost:8080", User: "admin", Password: "password", 144 ServerId: testServerId, ClientCertPath: "test/cert/path", ClientCertKeyPath: "test/cert/key/path", 145 IsDefault: false} 146 147 outputConfig, err := configAndGetTestServer(t, &inputDetails, false, interactive) 148 assert.NoError(t, err) 149 150 assert.Equal(t, "http://localhost:8080/", outputConfig.GetUrl()) 151 assert.Equal(t, "http://localhost:8080/artifactory/", outputConfig.GetArtifactoryUrl()) 152 assert.Equal(t, "http://localhost:8080/distribution/", outputConfig.GetDistributionUrl()) 153 assert.Equal(t, "http://localhost:8080/xray/", outputConfig.GetXrayUrl()) 154 assert.Equal(t, "http://localhost:8080/mc/", outputConfig.GetMissionControlUrl()) 155 assert.Equal(t, "http://localhost:8080/pipelines/", outputConfig.GetPipelinesUrl()) 156 157 inputDetails.ArtifactoryUrl = "http://localhost:8081/artifactory" 158 inputDetails.DistributionUrl = "http://localhost:8081/distribution" 159 inputDetails.XrayUrl = "http://localhost:8081/xray" 160 inputDetails.MissionControlUrl = "http://localhost:8081/mc" 161 inputDetails.PipelinesUrl = "http://localhost:8081/pipelines" 162 inputDetails.AccessUrl = "http://localhost:8081/access" 163 164 outputConfig, err = configAndGetTestServer(t, &inputDetails, false, interactive) 165 assert.NoError(t, err) 166 167 assert.Equal(t, "http://localhost:8080/", outputConfig.GetUrl()) 168 assert.Equal(t, "http://localhost:8081/artifactory/", outputConfig.GetArtifactoryUrl()) 169 assert.Equal(t, "http://localhost:8081/distribution/", outputConfig.GetDistributionUrl()) 170 assert.Equal(t, "http://localhost:8081/xray/", outputConfig.GetXrayUrl()) 171 assert.Equal(t, "http://localhost:8081/mc/", outputConfig.GetMissionControlUrl()) 172 assert.Equal(t, "http://localhost:8081/pipelines/", outputConfig.GetPipelinesUrl()) 173 } 174 175 func TestBasicAuthOnlyOption(t *testing.T) { 176 inputDetails := tests.CreateTestServerDetails() 177 inputDetails.User = "admin" 178 inputDetails.Password = "password" 179 180 // Verify setting the option disables refreshable tokens. 181 outputConfig, err := configAndGetTestServer(t, inputDetails, true, false) 182 assert.NoError(t, err) 183 assert.Equal(t, coreutils.TokenRefreshDisabled, outputConfig.ArtifactoryTokenRefreshInterval, "expected refreshable token to be disabled") 184 assert.NoError(t, NewConfigCommand(Delete, testServerId).Run()) 185 186 // Verify setting the option enables refreshable tokens. 187 outputConfig, err = configAndGetTestServer(t, inputDetails, false, false) 188 assert.NoError(t, err) 189 assert.Equal(t, coreutils.TokenRefreshDefaultInterval, outputConfig.ArtifactoryTokenRefreshInterval, "expected refreshable token to be enabled") 190 assert.NoError(t, NewConfigCommand(Delete, testServerId).Run()) 191 } 192 193 func TestMakeDefaultOption(t *testing.T) { 194 cleanUpJfrogHome, err := utilsTests.SetJfrogHome() 195 assert.NoError(t, err) 196 defer cleanUpJfrogHome() 197 198 originalDefault := tests.CreateTestServerDetails() 199 originalDefault.ServerId = "originalDefault" 200 originalDefault.IsDefault = false 201 newDefault := tests.CreateTestServerDetails() 202 newDefault.ServerId = "newDefault" 203 newDefault.IsDefault = false 204 205 // Config the first server, and expect it to be default because it is the only server. 206 configAndAssertDefault(t, originalDefault, false) 207 defer deleteServer(t, originalDefault.ServerId) 208 209 // Config a second server and pass the makeDefault option. 210 configAndAssertDefault(t, newDefault, true) 211 defer deleteServer(t, newDefault.ServerId) 212 } 213 214 func configAndAssertDefault(t *testing.T, inputDetails *config.ServerDetails, makeDefault bool) { 215 outputConfig, err := configAndGetServer(t, inputDetails.ServerId, inputDetails, false, false, makeDefault) 216 assert.NoError(t, err) 217 assert.Equal(t, inputDetails.ServerId, outputConfig.ServerId) 218 assert.True(t, outputConfig.IsDefault) 219 } 220 221 func deleteServer(t *testing.T, serverId string) { 222 assert.NoError(t, NewConfigCommand(Delete, serverId).Run()) 223 } 224 225 type unsafeUrlTest struct { 226 url string 227 isSafe bool 228 } 229 230 var unsafeUrlTestCases = []unsafeUrlTest{ 231 // Safe URLs 232 {"https://acme.jfrog.io", true}, 233 {"http://127.0.0.1", true}, 234 {"http://localhost", true}, 235 {"http://127.0.0.1:8081", true}, 236 {"http://localhost:8081", true}, 237 {"ssh://localhost:1339/", true}, 238 239 // Unsafe URLs: 240 {"http://acme.jfrog.io", false}, 241 {"http://acme.jfrog.io:8081", false}, 242 {"http://localhost-123", false}, 243 } 244 245 func TestAssertUrlsSafe(t *testing.T) { 246 for _, testCase := range unsafeUrlTestCases { 247 t.Run(testCase.url, func(t *testing.T) { 248 // Test non-interactive - should pass with a warning message 249 inputDetails := &config.ServerDetails{Url: testCase.url, ServerId: testServerId} 250 configAndTest(t, inputDetails, false) 251 252 // Test interactive - should fail with an error 253 configCmd := NewConfigCommand(AddOrEdit, testServerId).SetDetails(inputDetails).SetInteractive(true) 254 configCmd.disablePrompts = true 255 err := configCmd.Run() 256 if testCase.isSafe { 257 assert.NoError(t, err) 258 } else { 259 assert.ErrorContains(t, err, "config was aborted due to an insecure HTTP connection") 260 } 261 }) 262 } 263 } 264 265 func TestExportEmptyConfig(t *testing.T) { 266 cliHome, exist := os.LookupEnv(coreutils.HomeDir) 267 defer func() { 268 if exist { 269 assert.NoError(t, os.Setenv(coreutils.HomeDir, cliHome)) 270 } else { 271 assert.NoError(t, os.Unsetenv(coreutils.HomeDir)) 272 } 273 }() 274 tempDirPath, err := fileutils.CreateTempDir() 275 assert.NoError(t, err) 276 defer func() { 277 assert.NoError(t, fileutils.RemoveTempDir(tempDirPath), "Couldn't remove temp dir") 278 }() 279 assert.NoError(t, os.Setenv(coreutils.HomeDir, tempDirPath)) 280 assert.Error(t, Export("")) 281 } 282 283 func TestKeyEncryption(t *testing.T) { 284 cleanUpJfrogHome, err := utilsTests.SetJfrogHome() 285 assert.NoError(t, err) 286 defer cleanUpJfrogHome() 287 288 assert.NoError(t, os.Setenv(coreutils.EncryptionKey, "p3aNuTbUtt3rJ3lly&ChEEsEPlEasE!!")) 289 defer func() { 290 assert.NoError(t, os.Unsetenv(coreutils.EncryptionKey)) 291 }() 292 inputDetails := tests.CreateTestServerDetails() 293 inputDetails.User = "admin" 294 inputDetails.Password = "password" 295 296 configAndTest(t, inputDetails, true) 297 configAndTest(t, inputDetails, false) 298 } 299 300 func TestKeyDecryptionError(t *testing.T) { 301 cleanUpJfrogHome, err := utilsTests.SetJfrogHome() 302 assert.NoError(t, err) 303 defer cleanUpJfrogHome() 304 305 assert.NoError(t, os.Setenv(coreutils.EncryptionKey, "p3aNuTbUtt3rJ3lly&ChEEsEPlEasE!!")) 306 defer func() { 307 assert.NoError(t, os.Unsetenv(coreutils.EncryptionKey)) 308 }() 309 310 inputDetails := tests.CreateTestServerDetails() 311 inputDetails.User = "admin" 312 inputDetails.Password = "password" 313 314 // Configure server with JFROG_CLI_ENCRYPTION_KEY set 315 configCmd := NewConfigCommand(AddOrEdit, testServerId).SetDetails(inputDetails).SetUseBasicAuthOnly(true).SetInteractive(false) 316 configCmd.disablePrompts = true 317 assert.NoError(t, configCmd.Run()) 318 319 // Get the server details when JFROG_CLI_ENCRYPTION_KEY is not set and expect an error 320 assert.NoError(t, os.Unsetenv(coreutils.EncryptionKey)) 321 _, err = GetConfig(testServerId, false) 322 assert.ErrorContains(t, err, "cannot decrypt config") 323 } 324 325 func TestImport(t *testing.T) { 326 // Create temp jfrog home 327 cleanUpJfrogHome, err := utilsTests.SetJfrogHome() 328 assert.NoError(t, err) 329 defer cleanUpJfrogHome() 330 331 // Import config token 332 assert.NoError(t, Import(acmeConfigToken)) 333 serverDetails, err := GetConfig("acme", true) 334 assert.NoError(t, err) 335 336 // Verify that the configuration was imported correctly 337 assert.Equal(t, "https://acme.jfrog.io/", serverDetails.GetUrl()) 338 assert.Equal(t, "https://acme.jfrog.io/artifactory/", serverDetails.GetArtifactoryUrl()) 339 assert.Equal(t, "https://acme.jfrog.io/distribution/", serverDetails.GetDistributionUrl()) 340 assert.Equal(t, "https://acme.jfrog.io/xray/", serverDetails.GetXrayUrl()) 341 assert.Equal(t, "https://acme.jfrog.io/mc/", serverDetails.GetMissionControlUrl()) 342 assert.Equal(t, "https://acme.jfrog.io/pipelines/", serverDetails.GetPipelinesUrl()) 343 assert.Equal(t, "admin", serverDetails.GetUser()) 344 assert.Equal(t, "password", serverDetails.GetPassword()) 345 } 346 347 func testExportImport(t *testing.T, inputDetails *config.ServerDetails) { 348 configToken, err := config.Export(inputDetails) 349 assert.NoError(t, err) 350 outputDetails, err := config.Import(configToken) 351 assert.NoError(t, err) 352 assert.Equal(t, configStructToString(t, inputDetails), configStructToString(t, outputDetails), "unexpected configuration was saved to file") 353 } 354 355 func configAndTest(t *testing.T, inputDetails *config.ServerDetails, interactive bool) { 356 outputConfig, err := configAndGetTestServer(t, inputDetails, true, interactive) 357 assert.NoError(t, err) 358 assert.Equal(t, configStructToString(t, inputDetails), configStructToString(t, outputConfig), "unexpected configuration was saved to file") 359 assert.NoError(t, NewConfigCommand(Delete, testServerId).Run()) 360 testExportImport(t, inputDetails) 361 } 362 363 func configAndGetTestServer(t *testing.T, inputDetails *config.ServerDetails, basicAuthOnly, interactive bool) (*config.ServerDetails, error) { 364 return configAndGetServer(t, testServerId, inputDetails, basicAuthOnly, interactive, false) 365 } 366 367 func configAndGetServer(t *testing.T, serverId string, inputDetails *config.ServerDetails, basicAuthOnly, interactive, makeDefault bool) (*config.ServerDetails, error) { 368 doConfig(t, serverId, inputDetails, basicAuthOnly, interactive, makeDefault) 369 return GetConfig(serverId, false) 370 } 371 372 func doConfig(t *testing.T, serverId string, inputDetails *config.ServerDetails, basicAuthOnly, interactive, makeDefault bool) { 373 configCmd := NewConfigCommand(AddOrEdit, serverId).SetDetails(inputDetails).SetUseBasicAuthOnly(basicAuthOnly). 374 SetInteractive(interactive).SetMakeDefault(makeDefault) 375 configCmd.disablePrompts = true 376 assert.NoError(t, configCmd.Run()) 377 } 378 379 func configStructToString(t *testing.T, artConfig *config.ServerDetails) string { 380 artConfig.IsDefault = false 381 marshaledStruct, err := json.Marshal(*artConfig) 382 assert.NoError(t, err) 383 return string(marshaledStruct) 384 }