Skip to content

Commit bb36015

Browse files
committed
Improve repositores code
Signed-off-by: Michael Sverdlov <[email protected]>
1 parent 1ef7d0e commit bb36015

File tree

10 files changed

+732
-17
lines changed

10 files changed

+732
-17
lines changed

artifactory/commands/npm/login.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package npm
2+
3+
import (
4+
"github.com/jfrog/gofrog/version"
5+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/repository"
6+
cmdutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
7+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
8+
"github.com/jfrog/jfrog-cli-core/v2/common/project"
9+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
10+
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
11+
"github.com/jfrog/jfrog-client-go/artifactory/services"
12+
"github.com/jfrog/jfrog-client-go/utils/log"
13+
)
14+
15+
type NpmLoginCommand struct {
16+
commandName string
17+
npmVersion *version.Version
18+
repoUrl string
19+
executablePath string
20+
CommonArgs
21+
}
22+
23+
func NewNpmLoginCommand() *NpmLoginCommand {
24+
return &NpmLoginCommand{commandName: "rt_npm_login"}
25+
}
26+
27+
// Run configures npm to use the specified or selected JFrog Artifactory repository
28+
// for package management, setting up registry and authentication.
29+
func (nlc *NpmLoginCommand) Run() (err error) {
30+
// If no repository is specified, prompt the user to select an npm-compatible repository.
31+
if nlc.repo == "" {
32+
// Define filter parameters to select virtual repositories of npm package type.
33+
repoFilterParams := services.RepositoriesFilterParams{
34+
RepoType: utils.Virtual.String(),
35+
PackageType: repository.Npm,
36+
}
37+
38+
// Select repository interactively based on filter parameters and server details.
39+
nlc.repo, err = utils.SelectRepositoryInteractively(nlc.serverDetails, repoFilterParams)
40+
if err != nil {
41+
return err
42+
}
43+
}
44+
45+
// Initialize NpmrcYarnrcManager for npm to manage registry and authentication configurations.
46+
npmrcManager := cmdutils.NewNpmrcYarnrcManager(project.Npm, nlc.repo, nlc.serverDetails)
47+
48+
// Configure the registry URL for npm in the npm configuration.
49+
if err = npmrcManager.ConfigureRegistry(); err != nil {
50+
return err
51+
}
52+
53+
// Configure authentication settings, handling token or basic auth as needed.
54+
if err = npmrcManager.ConfigureAuth(); err != nil {
55+
return err
56+
}
57+
58+
// Output success message indicating successful npm configuration.
59+
log.Output(coreutils.PrintTitle("Successfully configured npm client to work with your JFrog Artifactory repository: " + nlc.repo))
60+
return nil
61+
}
62+
63+
func (nlc *NpmLoginCommand) CommandName() string {
64+
return nlc.commandName
65+
}
66+
67+
func (nlc *NpmLoginCommand) ServerDetails() (*config.ServerDetails, error) {
68+
return nlc.serverDetails, nil
69+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package npm
2+
3+
import (
4+
"fmt"
5+
cmdutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
6+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
7+
"github.com/stretchr/testify/assert"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
)
12+
13+
func TestNpmLoginCommand(t *testing.T) {
14+
// Create a temporary directory to act as the environment's npmrc file location.
15+
tempDir := t.TempDir()
16+
npmrcFilePath := filepath.Join(tempDir, ".npmrc")
17+
18+
// Set NPM_CONFIG_USERCONFIG to point to the temporary npmrc file path.
19+
t.Setenv("NPM_CONFIG_USERCONFIG", npmrcFilePath)
20+
21+
// Initialize a new NpmLoginCommand instance.
22+
loginCmd := NewNpmLoginCommand()
23+
loginCmd.SetServerDetails(&config.ServerDetails{ArtifactoryUrl: "https://acme.jfrog.io/artifactory"})
24+
loginCmd.repo = "npm-virtual"
25+
26+
// Define test cases for different authentication types.
27+
testCases := []struct {
28+
name string
29+
user string
30+
password string
31+
accessToken string
32+
}{
33+
{
34+
name: "Token Authentication",
35+
accessToken: "test-token",
36+
},
37+
{
38+
name: "Basic Authentication",
39+
user: "myUser",
40+
password: "myPassword",
41+
},
42+
{
43+
name: "Anonymous Access",
44+
},
45+
}
46+
47+
for _, testCase := range testCases {
48+
t.Run(testCase.name, func(t *testing.T) {
49+
// Set up server details for the current test case's authentication type.
50+
loginCmd.serverDetails.SetUser(testCase.user)
51+
loginCmd.serverDetails.SetPassword(testCase.password)
52+
loginCmd.serverDetails.SetAccessToken(testCase.accessToken)
53+
54+
// Run the login command and ensure no errors occur.
55+
assert.NoError(t, loginCmd.Run())
56+
57+
// Read the contents of the temporary npmrc file.
58+
npmrcContentBytes, err := os.ReadFile(npmrcFilePath)
59+
assert.NoError(t, err)
60+
npmrcContent := string(npmrcContentBytes)
61+
62+
// Validate that the registry URL was set correctly in .npmrc.
63+
assert.Contains(t, npmrcContent, fmt.Sprintf("%s=%s", cmdutils.NpmConfigRegistryKey, "https://acme.jfrog.io/artifactory/api/npm/npm-virtual"))
64+
65+
// Define expected keys for basic and token auth in the .npmrc.
66+
basicAuthKey := fmt.Sprintf("//acme.jfrog.io/artifactory/api/npm/npm-virtual:%s=", cmdutils.NpmConfigAuthKey)
67+
tokenAuthKey := fmt.Sprintf("//acme.jfrog.io/artifactory/api/npm/npm-virtual:%s=", cmdutils.NpmConfigAuthTokenKey)
68+
69+
switch {
70+
// Validate token-based authentication.
71+
case testCase.accessToken != "":
72+
assert.Contains(t, npmrcContent, tokenAuthKey+"test-token")
73+
assert.NotContains(t, npmrcContent, basicAuthKey)
74+
75+
// Validate basic authentication with encoded credentials.
76+
case testCase.user != "" && testCase.password != "":
77+
// Base64 encoding of "myUser:myPassword"
78+
expectedBasicAuth := basicAuthKey + "\"bXlVc2VyOm15UGFzc3dvcmQ=\""
79+
assert.Contains(t, npmrcContent, expectedBasicAuth)
80+
assert.NotContains(t, npmrcContent, tokenAuthKey)
81+
82+
// Validate anonymous access, where neither auth method is configured.
83+
default:
84+
assert.NotContains(t, npmrcContent, basicAuthKey)
85+
assert.NotContains(t, npmrcContent, tokenAuthKey)
86+
}
87+
})
88+
}
89+
}

artifactory/commands/utils/npmcmdutils.go

Lines changed: 135 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
package utils
22

33
import (
4+
"encoding/base64"
45
"fmt"
5-
outFormat "github.com/jfrog/jfrog-cli-core/v2/common/format"
6-
"net/http"
7-
"strings"
8-
96
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
7+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
8+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/yarn"
109
"github.com/jfrog/jfrog-cli-core/v2/common/build"
10+
outFormat "github.com/jfrog/jfrog-cli-core/v2/common/format"
11+
"github.com/jfrog/jfrog-cli-core/v2/common/project"
12+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
1113
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
1214
"github.com/jfrog/jfrog-client-go/auth"
1315
"github.com/jfrog/jfrog-client-go/http/httpclient"
1416
clientutils "github.com/jfrog/jfrog-client-go/utils"
1517
"github.com/jfrog/jfrog-client-go/utils/errorutils"
1618
"github.com/jfrog/jfrog-client-go/utils/log"
19+
"net/http"
20+
"strings"
1721
)
1822

1923
const (
2024
minSupportedArtifactoryVersionForNpmCmds = "5.5.2"
21-
NpmConfigAuthKey = "_auth"
22-
NpmConfigAuthTokenKey = "_authToken"
23-
npmAuthRestApi = "api/npm/auth"
25+
26+
NpmConfigAuthKey = "_auth"
27+
// Supported only in npm version 9 and above.
28+
NpmConfigAuthTokenKey = "_authToken"
29+
NpmConfigRegistryKey = "registry"
30+
npmAuthRestApi = "api/npm/auth"
2431
)
2532

2633
// Constructs npm auth config and registry, manually or by requesting the Artifactory /npm/auth endpoint.
@@ -37,7 +44,7 @@ func GetArtifactoryNpmRepoDetails(repo string, authArtDetails auth.ServiceDetail
3744
return "", "", err
3845
}
3946

40-
registry = getNpmRepositoryUrl(repo, authArtDetails.GetUrl())
47+
registry = GetNpmRepositoryUrl(repo, authArtDetails.GetUrl())
4148
return
4249
}
4350

@@ -59,7 +66,7 @@ func getNpmAuth(authArtDetails auth.ServiceDetails, isNpmAuthLegacyVersion bool)
5966

6067
// Manually constructs the npm authToken config data.
6168
func constructNpmAuthToken(token string) string {
62-
return fmt.Sprintf("%s = %s\nalways-auth = true", NpmConfigAuthTokenKey, token)
69+
return fmt.Sprintf("%s = %s", NpmConfigAuthTokenKey, token)
6370
}
6471

6572
func validateArtifactoryVersionForNpmCmds(artDetails auth.ServiceDetails) error {
@@ -93,12 +100,8 @@ func getNpmAuthFromArtifactory(artDetails auth.ServiceDetails) (npmAuth string,
93100
return string(body), nil
94101
}
95102

96-
func getNpmRepositoryUrl(repo, url string) string {
97-
if !strings.HasSuffix(url, "/") {
98-
url += "/"
99-
}
100-
url += "api/npm/" + repo
101-
return url
103+
func GetNpmRepositoryUrl(repositoryName, artifactoryUrl string) string {
104+
return strings.TrimSuffix(artifactoryUrl, "/") + "/api/npm/" + repositoryName
102105
}
103106

104107
// Remove all the none npm CLI flags from args.
@@ -125,3 +128,120 @@ func ExtractNpmOptionsFromArgs(args []string) (detailedSummary, xrayScan bool, s
125128
cleanArgs, buildConfig, err = build.ExtractBuildDetailsFromArgs(cleanArgs)
126129
return
127130
}
131+
132+
// NpmrcYarnrcManager is responsible for configuring npm and Yarn registries
133+
// and authentication settings based on the specified project type.
134+
type NpmrcYarnrcManager struct {
135+
// buildTool represents the project type, either NPM or Yarn.
136+
buildTool project.ProjectType
137+
// repoUrl holds the URL to the npm or Yarn repository.
138+
repoUrl string
139+
// serverDetails contains configuration details for the Artifactory server.
140+
serverDetails *config.ServerDetails
141+
}
142+
143+
// NewNpmrcYarnrcManager initializes a new NpmrcYarnrcManager with the given project type,
144+
// repository name, and Artifactory server details.
145+
func NewNpmrcYarnrcManager(buildTool project.ProjectType, repoName string, serverDetails *config.ServerDetails) *NpmrcYarnrcManager {
146+
repoUrl := GetNpmRepositoryUrl(repoName, serverDetails.ArtifactoryUrl)
147+
return &NpmrcYarnrcManager{
148+
buildTool: buildTool,
149+
repoUrl: repoUrl,
150+
serverDetails: serverDetails,
151+
}
152+
}
153+
154+
// ConfigureRegistry sets the registry URL in the npmrc or yarnrc file.
155+
func (nm *NpmrcYarnrcManager) ConfigureRegistry() error {
156+
return nm.configSet(NpmConfigRegistryKey, nm.repoUrl)
157+
}
158+
159+
// ConfigureAuth configures authentication in npmrc or yarnrc using token or basic auth,
160+
// or clears authentication for anonymous access.
161+
func (nm *NpmrcYarnrcManager) ConfigureAuth() error {
162+
authArtDetails, err := nm.serverDetails.CreateArtAuthConfig()
163+
if err != nil {
164+
return err
165+
}
166+
167+
// Configure authentication based on available credentials.
168+
switch {
169+
case authArtDetails.GetAccessToken() != "":
170+
return nm.handleNpmrcTokenAuth(authArtDetails.GetAccessToken())
171+
case authArtDetails.GetUser() != "" && authArtDetails.GetPassword() != "":
172+
return nm.handleNpmrcBasicAuth(authArtDetails.GetUser(), authArtDetails.GetPassword())
173+
default:
174+
return nm.handleNpmAnonymousAccess()
175+
}
176+
}
177+
178+
// handleNpmrcTokenAuth sets the token in the npmrc or yarnrc file and clears basic auth if it exists.
179+
func (nm *NpmrcYarnrcManager) handleNpmrcTokenAuth(token string) error {
180+
authKey := nm.createAuthKey(NpmConfigAuthTokenKey)
181+
if err := nm.configSet(authKey, token); err != nil {
182+
return err
183+
}
184+
return nm.removeNpmrcBasicAuthIfExists()
185+
}
186+
187+
// handleNpmrcBasicAuth sets basic auth credentials and clears any token-based auth.
188+
func (nm *NpmrcYarnrcManager) handleNpmrcBasicAuth(user, password string) error {
189+
authKey := nm.createAuthKey(NpmConfigAuthKey)
190+
authValue := basicAuthBase64Encode(user, password)
191+
if err := nm.configSet(authKey, authValue); err != nil {
192+
return err
193+
}
194+
return nm.removeNpmrcTokenAuthIfExists()
195+
}
196+
197+
// handleNpmAnonymousAccess removes any existing authentication settings for anonymous access.
198+
func (nm *NpmrcYarnrcManager) handleNpmAnonymousAccess() error {
199+
if err := nm.removeNpmrcBasicAuthIfExists(); err != nil {
200+
return err
201+
}
202+
return nm.removeNpmrcTokenAuthIfExists()
203+
}
204+
205+
// removeNpmrcBasicAuthIfExists deletes basic auth credentials if present.
206+
func (nm *NpmrcYarnrcManager) removeNpmrcBasicAuthIfExists() error {
207+
return nm.configDelete(nm.createAuthKey(NpmConfigAuthKey))
208+
}
209+
210+
// removeNpmrcTokenAuthIfExists deletes token auth credentials if present.
211+
func (nm *NpmrcYarnrcManager) removeNpmrcTokenAuthIfExists() error {
212+
return nm.configDelete(nm.createAuthKey(NpmConfigAuthTokenKey))
213+
}
214+
215+
// configSet applies a configuration setting in npmrc or yarnrc, based on the build tool type.
216+
func (nm *NpmrcYarnrcManager) configSet(key, value string) error {
217+
switch nm.buildTool {
218+
case project.Npm:
219+
return npm.ConfigSet(key, value, nm.buildTool.String())
220+
case project.Yarn:
221+
return yarn.ConfigSet(key, value, nm.buildTool.String(), false)
222+
default:
223+
return errorutils.CheckError(fmt.Errorf("unsupported build tool: %s", nm.buildTool))
224+
}
225+
}
226+
227+
// configDelete removes a configuration setting from npmrc or yarnrc, based on the build tool type.
228+
func (nm *NpmrcYarnrcManager) configDelete(key string) error {
229+
switch nm.buildTool {
230+
case project.Npm:
231+
return npm.ConfigDelete(key, nm.buildTool.String())
232+
case project.Yarn:
233+
return yarn.ConfigDelete(key, nm.buildTool.String())
234+
default:
235+
return errorutils.CheckError(fmt.Errorf("unsupported build tool: %s", nm.buildTool))
236+
}
237+
}
238+
239+
// createAuthKey generates the correct authentication key for npm or Yarn, based on the repo URL.
240+
func (nm *NpmrcYarnrcManager) createAuthKey(keySuffix string) string {
241+
return fmt.Sprintf("//%s:%s", strings.TrimPrefix(nm.repoUrl, "https://"), keySuffix)
242+
}
243+
244+
// basicAuthBase64Encode encodes user credentials in Base64 for basic authentication.
245+
func basicAuthBase64Encode(user, password string) string {
246+
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, password)))
247+
}

0 commit comments

Comments
 (0)