From a9640a68daa6cb4dc36ea7f7c882c6c337e13b30 Mon Sep 17 00:00:00 2001 From: Denis Kovalenchenko Date: Wed, 26 Jan 2022 19:46:21 +0300 Subject: [PATCH] Add npm-login goal to authenticate in private npm repository --- CHANGELOG.md | 3 + README.md | 35 +++ .../src/it/npm-login/pom.xml | 169 ++++++++++++++ .../maven/plugins/NpmRegistryMock.java | 37 +++ .../src/it/npm-login/user.home/empty/.npmrc | 0 .../it/npm-login/user.home/with_token/.npmrc | 2 + .../npm-login/user.home/without_token/.npmrc | 2 + .../src/it/npm-login/verify.groovy | 28 +++ frontend-maven-plugin/src/it/settings.xml | 7 + .../plugins/frontend/mojo/NpmLoginMojo.java | 221 ++++++++++++++++++ 10 files changed, 504 insertions(+) create mode 100644 frontend-maven-plugin/src/it/npm-login/pom.xml create mode 100644 frontend-maven-plugin/src/it/npm-login/src/main/java/com/github/eirslett/maven/plugins/NpmRegistryMock.java create mode 100644 frontend-maven-plugin/src/it/npm-login/user.home/empty/.npmrc create mode 100644 frontend-maven-plugin/src/it/npm-login/user.home/with_token/.npmrc create mode 100644 frontend-maven-plugin/src/it/npm-login/user.home/without_token/.npmrc create mode 100644 frontend-maven-plugin/src/it/npm-login/verify.groovy create mode 100644 frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/NpmLoginMojo.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f002ebdf..1acaacddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Last public release: [![Maven Central](https://maven-badges.herokuapp.com/maven- ## Changelog +### 1.12.1 +Add npm-login goal to authenticate in private npm repository + ### 1.12.1 * update Dependency: Jackson (2.13.0), Mockito (4.1.0), JUnit (5.8.1), Hamcrest (2.2; now a direct dependency) diff --git a/README.md b/README.md index 4dee31f74..6008eb3f2 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,40 @@ will help to separate your frontend and backend builds even more. ``` +### Running Npm Login + +```xml + + npm login + + npm-login + + + + generate-resources + + + https://npm-registry.com + npm-registry + + +``` + +and server section in ~/.m2/settings.xml +```xml + + + ... + + + npm-registry + username + password + + + ... +``` + ### Optional Configuration #### Working directory @@ -533,6 +567,7 @@ Tools and property to enable skipping * jspm `-Dskip.jspm` * karma `-Dskip.karma` * webpack `-Dskip.webpack` +* npm-login `-Dskip.npm-login` ## Eclipse M2E support diff --git a/frontend-maven-plugin/src/it/npm-login/pom.xml b/frontend-maven-plugin/src/it/npm-login/pom.xml new file mode 100644 index 000000000..a7923d8c4 --- /dev/null +++ b/frontend-maven-plugin/src/it/npm-login/pom.xml @@ -0,0 +1,169 @@ + + + 4.0.0 + + com.github.eirslett + example + 0 + pom + + + UTF-8 + 1.8 + 1.8 + + + + + org.mock-server + mockserver-client-java + 5.11.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + npm-registry-mock-server-compile + generate-resources + + compile + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + generate random port + generate-resources + + reserve-network-port + + + true + + npm.registry.port + + + + + + + + maven-resources-plugin + + + filter .npmrc zand verify.groovy + generate-resources + + copy-resources + + + ${project.basedir} + true + + + ${project.basedir} + true + + verify.groovy + **/.npmrc + + + + + + + + + + org.mock-server + mockserver-maven-plugin + 5.11.2 + + ${npm.registry.port} + INFO + com.github.eirslett.maven.plugins.NpmRegistryMock + + + + npm-registry-mock-server-start + generate-resources + + start + + + + npm-registry-mock-server-stop + verify + + stop + + + + + + + com.github.eirslett + frontend-maven-plugin + + @project.version@ + + + target + + + + + npm login (empty) + + npm-login + + + http://localhost:${npm.registry.port} + npm-registry + + ${project.basedir}/user.home/empty/ + + + + + npm login (with_token) + + npm-login + + + http://localhost:${npm.registry.port} + npm-registry + + ${project.basedir}/user.home/with_token/ + + + + + npm login (without_token) + + npm-login + + + http://localhost:${npm.registry.port} + npm-registry + + ${project.basedir}/user.home/without_token/ + + + + + + + diff --git a/frontend-maven-plugin/src/it/npm-login/src/main/java/com/github/eirslett/maven/plugins/NpmRegistryMock.java b/frontend-maven-plugin/src/it/npm-login/src/main/java/com/github/eirslett/maven/plugins/NpmRegistryMock.java new file mode 100644 index 000000000..eb6dc60ea --- /dev/null +++ b/frontend-maven-plugin/src/it/npm-login/src/main/java/com/github/eirslett/maven/plugins/NpmRegistryMock.java @@ -0,0 +1,37 @@ +package com.github.eirslett.maven.plugins; + +import org.mockserver.client.MockServerClient; +import org.mockserver.client.initialize.PluginExpectationInitializer; + +import static org.mockserver.model.Header.header; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.HttpStatusCode.OK_200; + +public class NpmRegistryMock implements PluginExpectationInitializer { + private static final String NPM_USERNAME = "username"; + private static final String NPM_PASSWORD = "password"; + private static final String NPM_TOKEN = "NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459"; + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final String MIME_TYPE_APPLICATION_JSON = "application/json; charset=UTF-8"; + + @Override + public void initializeExpectations(MockServerClient mockServerClient) { + mockServerClient.when( + request() + .withMethod("PUT") + .withPath("/-/user/org.couchdb.user:" + NPM_USERNAME) + .withHeader(HEADER_CONTENT_TYPE, MIME_TYPE_APPLICATION_JSON) + .withBody("{\"name\":\"" + NPM_USERNAME + "\",\"password\":\"" + NPM_PASSWORD + "\"}") + ) + .respond( + response() + .withStatusCode(OK_200.code()) + .withHeaders( + header(HEADER_CONTENT_TYPE, MIME_TYPE_APPLICATION_JSON) + ) + .withBody("{\"rev\":\"_we_dont_use_revs_any_more\",\"id\":\"org.couchdb.user:undefined\"," + + "\"ok\":\"true\",\"token\":\"" + NPM_TOKEN + "\"}")); + } +} diff --git a/frontend-maven-plugin/src/it/npm-login/user.home/empty/.npmrc b/frontend-maven-plugin/src/it/npm-login/user.home/empty/.npmrc new file mode 100644 index 000000000..e69de29bb diff --git a/frontend-maven-plugin/src/it/npm-login/user.home/with_token/.npmrc b/frontend-maven-plugin/src/it/npm-login/user.home/with_token/.npmrc new file mode 100644 index 000000000..13273d6da --- /dev/null +++ b/frontend-maven-plugin/src/it/npm-login/user.home/with_token/.npmrc @@ -0,0 +1,2 @@ +//localhost:${npm.registry.port}/:_authToken="NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459" +registry=https://localhost:${npm.registry.port} diff --git a/frontend-maven-plugin/src/it/npm-login/user.home/without_token/.npmrc b/frontend-maven-plugin/src/it/npm-login/user.home/without_token/.npmrc new file mode 100644 index 000000000..89e7549ce --- /dev/null +++ b/frontend-maven-plugin/src/it/npm-login/user.home/without_token/.npmrc @@ -0,0 +1,2 @@ +//localhost/:_authToken="NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459" +registry=https://localhost diff --git a/frontend-maven-plugin/src/it/npm-login/verify.groovy b/frontend-maven-plugin/src/it/npm-login/verify.groovy new file mode 100644 index 000000000..21e6a5af4 --- /dev/null +++ b/frontend-maven-plugin/src/it/npm-login/verify.groovy @@ -0,0 +1,28 @@ +import java.nio.file.Paths + +def buildLog = new File(basedir, 'build.log').text +assert buildLog.contains('BUILD SUCCESS') : 'build was not successful' +assertEmpty() +assertWithToken() +assertWithoutToken() + +def assertEmpty() { + def npmrcFile = Paths.get(basedir.getAbsolutePath(), 'user.home', 'empty', '.npmrc').toFile() + assert npmrcFile.exists() : "${npmrcFile.getAbsolutePath()} does`exist"; + assert npmrcFile.text.equals('//localhost:${npm.registry.port}/:_authToken="NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459"\n') : "incorrect content" +} + +def assertWithToken() { + def buildLog = new File(basedir, 'build.log').text + assert buildLog.contains('Token exists. Skipping execution.') : 'build was not successful' + def npmrcFile = Paths.get(basedir.getAbsolutePath(), 'user.home', 'with_token', '.npmrc').toFile() + assert npmrcFile.exists() : "${npmrcFile.getAbsolutePath()} does`exist"; + assert npmrcFile.text.equals('//localhost:${npm.registry.port}/:_authToken="NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459"\nregistry=https://localhost:${npm.registry.port}\n') : "incorrect content" +} + +def assertWithoutToken() { + def npmrcFile = Paths.get(basedir.getAbsolutePath(), 'user.home', 'without_token', '.npmrc').toFile() + assert npmrcFile.exists() : "${npmrcFile.getAbsolutePath()} does`exist"; + assert npmrcFile.text.equals('//localhost:${npm.registry.port}/:_authToken="NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459"\n//localhost/:_authToken="NpmToken.22e3a730-9e62-11e8-98d0-529269fb1459"\n' + + 'registry=https://localhost\n') : "incorrect content" +} \ No newline at end of file diff --git a/frontend-maven-plugin/src/it/settings.xml b/frontend-maven-plugin/src/it/settings.xml index abfdc6b5b..4aeaf42d6 100644 --- a/frontend-maven-plugin/src/it/settings.xml +++ b/frontend-maven-plugin/src/it/settings.xml @@ -1,5 +1,12 @@ + + + npm-registry + username + password + + mrm-maven-plugin diff --git a/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/NpmLoginMojo.java b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/NpmLoginMojo.java new file mode 100644 index 000000000..844d9aac0 --- /dev/null +++ b/frontend-maven-plugin/src/main/java/com/github/eirslett/maven/plugins/frontend/mojo/NpmLoginMojo.java @@ -0,0 +1,221 @@ +package com.github.eirslett.maven.plugins.frontend.mojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.crypto.SettingsDecrypter; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.github.eirslett.maven.plugins.frontend.lib.ProxyConfig.Proxy; +import static org.apache.http.entity.ContentType.APPLICATION_JSON; + +@Mojo(name = "npm-login", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) +public class NpmLoginMojo extends AbstractMojo { + + @Parameter(property = "frontend.npm-login.npmInheritsProxyConfigFromMaven", defaultValue = "true") + private boolean npmInheritsProxyConfigFromMaven; + + /** + * Registry override, passed as the registry option during npm install if set. + */ + @Parameter(property = "npmRegistryURL", required = true) + private String npmRegistryURL; + + /** + * Server Id for access to npm registry + */ + @Parameter(property = "npmRegistryServerId", required = true) + private String npmRegistryServerId; + + @Parameter(property = "session", defaultValue = "${session}", readonly = true) + private MavenSession session; + + @Component(role = SettingsDecrypter.class) + private SettingsDecrypter decrypter; + + /** + * Skips execution of this mojo. + */ + @Parameter(property = "skip.npm-login", defaultValue = "${skip.npm-login}") + private boolean skip; + + @Parameter(property = "userHome", defaultValue = "${user.home}") + private String userHome; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + getLog().info("Skipping execution."); + return; + } + + String npmRegistryAuth = "//" + npmRegistryURL + .replaceAll("http.?://", "") + .replaceFirst("/$", "") + "/"; + try { + Path npmrcPath = Paths.get(userHome, ".npmrc"); + int position = -1; + List lines; + if (Files.exists(npmrcPath)) { + lines = Files.readAllLines(npmrcPath); + int i = -1; + for (String line : lines) { + if (line.startsWith(npmRegistryAuth)) { + getLog().info("Token exists. Skipping execution."); + return; + } + + i++; + if (position == -1 && line.startsWith("//") && line.contains(":_authToken=")) { + position = i; + } + } + } else { + lines = new ArrayList<>(); + position = 0; + Files.createFile(npmrcPath); + } + lines.add(Math.max(position, 0), npmRegistryAuth + ":_authToken=\"" + getToken() + "\""); + Files.write(npmrcPath, lines); + } catch (MojoExecutionException | MojoFailureException e) { + throw e; + } catch (Exception e) { + throw MojoUtils.toMojoFailureException(e); + } + } + + + private String getToken() throws Exception { + Server server = MojoUtils.decryptServer(npmRegistryServerId, session, decrypter); + if (server == null) { + throw new IllegalStateException("Unknown serverId: " + npmRegistryServerId); + } + String npmRegistryUsername = server.getUsername(); + String npmRegistryPassword = server.getPassword(); + + ProxyConfig proxyConfig; + if (npmInheritsProxyConfigFromMaven) { + proxyConfig = MojoUtils.getProxyConfig(session, decrypter); + } else { + getLog().info("npm-login not inheriting proxy config from Maven"); + proxyConfig = new ProxyConfig(Collections.emptyList()); + } + + HttpClientBuilder httpClientBuilder = HttpClients.custom() + .disableContentCompression() + .useSystemProperties(); + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + Proxy proxy = proxyConfig.getProxyForUrl(npmRegistryURL); + + if (proxy != null) { + if (proxy.useAuthentication()) { + final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope(proxy.host, proxy.port), + new UsernamePasswordCredentials(proxy.username, proxy.password) + ); + + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + HttpHost proxyHttpHost = new HttpHost(proxy.host, proxy.port); + requestConfigBuilder.setProxy(proxyHttpHost); + } + + ObjectMapper objectMapper = new ObjectMapper(); + HttpPut httpPut = new HttpPut(npmRegistryURL.replaceFirst("/$", "") + "/-/user/org.couchdb.user:" + npmRegistryUsername); + httpPut.setConfig(requestConfigBuilder.build()); + TokenRequest tokenRequest = new TokenRequest(npmRegistryUsername, npmRegistryPassword); + httpPut.setEntity(new StringEntity(objectMapper.writeValueAsString(tokenRequest), APPLICATION_JSON)); + + try (CloseableHttpClient client = HttpClients.createDefault()) { + CloseableHttpResponse response = client.execute(httpPut); + TokenResponse tokenResponse = objectMapper.readValue(response.getEntity().getContent(), TokenResponse.class); + if (tokenResponse.getError() != null) { + throw new MojoFailureException("Error get token: " + tokenResponse.getError()); + } + return tokenResponse.getToken(); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenRequest { + private String name; + private String password; + + public TokenRequest() { + } + + public TokenRequest(String name, String password) { + this.name = name; + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenResponse { + private String token; + private String error; + + public TokenResponse() { + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + } +}