Skip to content

Commit

Permalink
Add support for Maven username & password authentication
Browse files Browse the repository at this point in the history
Progress on issue #264.

RELNOTES: Maven servers that require username & password authentication are
now supported (see maven_server documentation).

--
MOS_MIGRATED_REVID=103583838
  • Loading branch information
kchodorow authored and laszlocsomor committed Sep 22, 2015
1 parent 37738ca commit be11733
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.devtools.build.lib.analysis.RuleDefinition;
Expand All @@ -37,16 +38,21 @@
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;

import org.apache.maven.settings.Server;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;

import java.io.IOException;
import java.util.Map;

import javax.annotation.Nullable;

Expand All @@ -65,7 +71,7 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctio
return null;
}

String url;
MavenServerValue serverValue;
AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
boolean hasRepository = mapper.has("repository", Type.STRING)
&& !mapper.get("repository", Type.STRING).isEmpty();
Expand All @@ -77,30 +83,28 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctio
+ "'repository' and 'server', which are mutually exclusive options"),
Transience.PERSISTENT);
} else if (hasRepository) {
url = mapper.get("repository", Type.STRING);
serverValue = MavenServerValue.createFromUrl(mapper.get("repository", Type.STRING));
} else {
String serverName = DEFAULT_SERVER;
if (mapper.has("server", Type.STRING) && !mapper.get("server", Type.STRING).isEmpty()) {
serverName = mapper.get("server", Type.STRING);
}

MavenServerValue mavenServerValue = (MavenServerValue) env.getValue(
MavenServerValue.key(serverName));
if (mavenServerValue == null) {
serverValue = (MavenServerValue) env.getValue(MavenServerValue.key(serverName));
if (serverValue == null) {
return null;
}
url = mavenServerValue.getUrl();
}

MavenDownloader downloader = createMavenDownloader(mapper, url);
MavenDownloader downloader = createMavenDownloader(mapper, serverValue);
return createOutputTree(downloader, env);
}

@VisibleForTesting
MavenDownloader createMavenDownloader(AttributeMap mapper, String url) {
MavenDownloader createMavenDownloader(AttributeMap mapper, MavenServerValue serverValue) {
String name = mapper.getName();
Path outputDirectory = getExternalRepositoryDirectory().getRelative(name);
return new MavenDownloader(name, mapper, outputDirectory, url);
return new MavenDownloader(name, mapper, outputDirectory, serverValue);
}

SkyValue createOutputTree(MavenDownloader downloader, Environment env)
Expand Down Expand Up @@ -159,9 +163,10 @@ static class MavenDownloader {
@Nullable
private final String sha1;
private final String url;
private final Server server;

public MavenDownloader(
String name, AttributeMap mapper, Path outputDirectory, String url) {
String name, AttributeMap mapper, Path outputDirectory, MavenServerValue serverValue) {
this.name = name;
this.outputDirectory = outputDirectory;

Expand All @@ -173,7 +178,8 @@ public MavenDownloader(
+ mapper.get("version", Type.STRING);
}
this.sha1 = (mapper.has("sha1", Type.STRING)) ? mapper.get("sha1", Type.STRING) : null;
this.url = url;
this.url = serverValue.getUrl();
this.server = serverValue.getServer();
}

/**
Expand All @@ -198,7 +204,11 @@ public Path download() throws IOException {
RepositorySystem system = connector.newRepositorySystem();
RepositorySystemSession session = connector.newRepositorySystemSession(system);

RemoteRepository repository = new RemoteRepository.Builder(name, "default", url).build();

RemoteRepository repository = new RemoteRepository.Builder(
name, MavenServerValue.DEFAULT_ID, url)
.setAuthentication(new MavenAuthentication(server))
.build();
ArtifactRequest artifactRequest = new ArtifactRequest();
Artifact artifact;
try {
Expand Down Expand Up @@ -230,4 +240,37 @@ public Path download() throws IOException {
}
}

private static class MavenAuthentication implements Authentication {

private final Map<String, String> authenticationInfo;

private MavenAuthentication(Server server) {
ImmutableMap.Builder builder = ImmutableMap.<String, String>builder();
// From https://maven.apache.org/settings.html: "If you use a private key to login to the
// server, make sure you omit the <password> element. Otherwise, the key will be ignored."
if (server.getPassword() != null) {
builder.put(AuthenticationContext.USERNAME, server.getUsername());
builder.put(AuthenticationContext.PASSWORD, server.getPassword());
} else if (server.getPrivateKey() != null) {
// getPrivateKey sounds like it returns the key, but it actually returns a path to it.
builder.put(AuthenticationContext.PRIVATE_KEY_PATH, server.getPrivateKey());
builder.put(AuthenticationContext.PRIVATE_KEY_PASSPHRASE, server.getPassphrase());
}
authenticationInfo = builder.build();
}

@Override
public void fill(
AuthenticationContext authenticationContext, String s, Map<String, String> map) {
for (Map.Entry<String, String> entry : authenticationInfo.entrySet()) {
authenticationContext.put(entry.getKey(), entry.getValue());
}
}

@Override
public void digest(AuthenticationDigest authenticationDigest) {
// No-op.
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package com.google.devtools.build.lib.bazel.repository;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.bazel.rules.workspace.MavenServerRule;
Expand All @@ -37,9 +39,9 @@
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingResult;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

import javax.annotation.Nullable;

Expand All @@ -49,6 +51,9 @@
public class MavenServerFunction extends RepositoryFunction {
public static final SkyFunctionName NAME = SkyFunctionName.create("MAVEN_SERVER_FUNCTION");

private static final String USER_KEY = "user";
private static final String SYSTEM_KEY = "system";

public MavenServerFunction(BlazeDirectories directories) {
setDirectories(directories);
}
Expand All @@ -58,48 +63,75 @@ public MavenServerFunction(BlazeDirectories directories) {
public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctionException {
String repository = skyKey.argument().toString();
Package externalPackage = RepositoryFunction.getExternalPackage(env);
if (externalPackage == null) {
return null;
}
Rule repositoryRule = externalPackage.getRule(repository);

String serverName;
String url;
Map<String, FileValue> settingsFiles;
boolean foundRepoRule = repositoryRule != null
&& repositoryRule.getRuleClass().equals(MavenServerRule.NAME);
if (!foundRepoRule) {
if (repository.equals(MavenServerValue.DEFAULT_ID)) {
// The default repository is being used and the WORKSPACE is not overriding the default.
return new MavenServerValue();
settingsFiles = getDefaultSettingsFile(env);
serverName = MavenServerValue.DEFAULT_ID;
url = MavenConnector.getMavenCentralRemote().getUrl();
} else {
throw new RepositoryFunctionException(
new IOException("Could not find maven repository " + repository),
Transience.TRANSIENT);
}
throw new RepositoryFunctionException(
new IOException("Could not find maven repository " + repository), Transience.TRANSIENT);
}
} else {
AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(repositoryRule);
serverName = repositoryRule.getName();
url = mapper.get("url", Type.STRING);
if (!mapper.has("settings_file", Type.STRING)
|| mapper.get("settings_file", Type.STRING).isEmpty()) {
settingsFiles = getDefaultSettingsFile(env);
} else {
PathFragment settingsFilePath = new PathFragment(mapper.get("settings_file", Type.STRING));
RootedPath settingsPath = RootedPath.toRootedPath(
getWorkspace().getRelative(settingsFilePath), PathFragment.EMPTY_FRAGMENT);
FileValue fileValue = (FileValue) env.getValue(FileValue.key(settingsPath));
if (fileValue == null) {
return null;
}

AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(repositoryRule);
String serverName = repositoryRule.getName();
String url = mapper.get("url", Type.STRING);
if (!mapper.has("settings_file", Type.STRING)
|| mapper.get("settings_file", Type.STRING).isEmpty()) {
return new MavenServerValue(serverName, url, new Server());
if (!fileValue.exists()) {
throw new RepositoryFunctionException(
new IOException("Could not find settings file " + settingsPath),
Transience.TRANSIENT);
}
settingsFiles = ImmutableMap.<String, FileValue>builder().put(
USER_KEY, fileValue).build();
}
}
PathFragment settingsFilePath = new PathFragment(mapper.get("settings_file", Type.STRING));
RootedPath settingsPath = RootedPath.toRootedPath(
getWorkspace().getRelative(settingsFilePath), PathFragment.EMPTY_FRAGMENT);
FileValue settingsFile = (FileValue) env.getValue(FileValue.key(settingsPath));
if (settingsFile == null) {
if (settingsFiles == null) {
return null;
}

if (!settingsFile.exists()) {
throw new RepositoryFunctionException(
new IOException("Could not find settings file " + settingsPath), Transience.TRANSIENT);
if (settingsFiles.isEmpty()) {
return new MavenServerValue(serverName, url, new Server());
}

DefaultSettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
request.setUserSettingsFile(new File(settingsFile.realRootedPath().asPath().toString()));
if (settingsFiles.containsKey(SYSTEM_KEY)) {
request.setGlobalSettingsFile(
settingsFiles.get(SYSTEM_KEY).realRootedPath().asPath().getPathFile());
}
if (settingsFiles.containsKey(USER_KEY)) {
request.setUserSettingsFile(
settingsFiles.get(USER_KEY).realRootedPath().asPath().getPathFile());
}
DefaultSettingsBuilder builder = (new DefaultSettingsBuilderFactory()).newInstance();
SettingsBuildingResult result;
try {
result = builder.build(request);
} catch (SettingsBuildingException e) {
throw new RepositoryFunctionException(
new IOException("Error parsing settings file " + settingsFile + ": " + e.getMessage()),
new IOException("Error parsing settings files: " + e.getMessage()),
Transience.TRANSIENT);
}
if (!result.getProblems().isEmpty()) {
Expand All @@ -108,11 +140,53 @@ public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctio
+ Arrays.toString(result.getProblems().toArray())), Transience.PERSISTENT);
}
Settings settings = result.getEffectiveSettings();
Server server = settings.getServer(mapper.getName());
Server server = settings.getServer(serverName);
server = server == null ? new Server() : server;
return new MavenServerValue(serverName, url, server);
}

private Map<String, FileValue> getDefaultSettingsFile(Environment env) {
// The system settings file is at $M2_HOME/conf/settings.xml.
String m2Home = System.getenv("M2_HOME");
ImmutableList.Builder<SkyKey> settingsFilesBuilder = ImmutableList.builder();
SkyKey systemKey = null;
if (m2Home != null) {
PathFragment mavenInstallSettings = new PathFragment(m2Home).getRelative("conf/settings.xml");
systemKey = FileValue.key(
RootedPath.toRootedPath(getWorkspace().getRelative(mavenInstallSettings),
PathFragment.EMPTY_FRAGMENT));
settingsFilesBuilder.add(systemKey);
}

// The user settings file is at $HOME/.m2/settings.xml.
String userHome = System.getenv("HOME");
SkyKey userKey = null;
if (userHome != null) {
PathFragment userSettings = new PathFragment(userHome).getRelative(".m2/settings.xml");
userKey = FileValue.key(RootedPath.toRootedPath(getWorkspace().getRelative(userSettings),
PathFragment.EMPTY_FRAGMENT));
settingsFilesBuilder.add(userKey);
}

ImmutableList settingsFiles = settingsFilesBuilder.build();
if (settingsFiles.isEmpty()) {
return ImmutableMap.of();
}
Map<SkyKey, SkyValue> values = env.getValues(settingsFilesBuilder.build());
ImmutableMap.Builder<String, FileValue> settingsBuilder = ImmutableMap.builder();
for (Map.Entry<SkyKey, SkyValue> entry : values.entrySet()) {
if (entry.getValue() == null) {
return null;
}
if (systemKey != null && systemKey.equals(entry.getKey())) {
settingsBuilder.put(SYSTEM_KEY, (FileValue) entry.getValue());
} else if (userKey != null && userKey.equals(entry.getKey())) {
settingsBuilder.put(USER_KEY, (FileValue) entry.getValue());
}
}
return settingsBuilder.build();
}

@Override
public SkyFunctionName getSkyFunctionName() {
return NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public static SkyKey key(String serverName) {
return new SkyKey(MavenServerFunction.NAME, serverName);
}

public static MavenServerValue createFromUrl(String url) {
return new MavenServerValue(DEFAULT_ID, url, new Server());
}

public MavenServerValue() {
id = DEFAULT_ID;
url = MavenConnector.getMavenCentralRemote().getUrl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ <p>For example, Maven Central (which is the default and does not need to be defi
<!-- #END_BLAZE_RULE.ATTRIBUTE --> */
.add(attr("url", Type.STRING))
/* <!-- #BLAZE_RULE(maven_server).ATTRIBUTE(settings_file) -->
A path to a settings.xml file.
A path to a settings.xml file. Used for testing. If unspecified, this defaults to using
<code>$M2_HOME/conf/settings.xml</code> for the global settings and
<code>$HOME/.m2/settings.xml</code> for the user settings.
${SYNOPSIS}
<!-- #END_BLAZE_RULE.ATTRIBUTE --> */
.add(attr("settings_file", Type.STRING))
Expand Down Expand Up @@ -71,4 +73,33 @@ public Metadata getMetadata() {
<p>This is a combination of a &lt;repository&gt; definition from a pom.xml file and a
&lt;server&lt; definition from a settings.xml file.</p>
<h4>Using <code>maven_server</code></h4>
<p><code>maven_jar</code> rules can specify the name of a <code>maven_server</code> in their
<code>server</code> field. For example, suppose we have the following WORKSPACE file:</p>
<pre>
maven_jar(
name = "junit",
artifact = "junit:junit-dep:4.10",
server = "my-server",
)
maven_server(
name = "my-server",
url = "http://intranet.mycorp.net",
)
</pre>
This specifies that junit should be downloaded from http://intranet.mycorp.net using the
authentication information found in ~/.m2/settings.xml (specifically, the settings
for the server with the id <code>my-server</code>).
<h4>Specifying a default server</h4>
<p>If you create a <code>maven_server</code> with the <code>name</code> "default" it will be
used for any <code>maven_jar</code> that does not specify a <code>server</code> nor
<code>repository</code>. If there is no <code>maven_server</code> named default, the
default will be fetching from Maven Central with no authentication enabled.</p>
<!-- #END_BLAZE_RULE -->*/
1 change: 1 addition & 0 deletions src/test/shell/bazel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ filegroup(
"remote_helpers.sh",
"test-setup.sh",
"testenv.sh",
"testing_server.py",
":langtools-copy",
":objc-deps",
"//examples:srcs",
Expand Down
Loading

0 comments on commit be11733

Please sign in to comment.