DefaultSettingsBuilder.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.impl;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import org.apache.maven.api.Constants;
import org.apache.maven.api.ProtoSession;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.services.BuilderProblem;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.api.services.ProblemCollector;
import org.apache.maven.api.services.SettingsBuilder;
import org.apache.maven.api.services.SettingsBuilderException;
import org.apache.maven.api.services.SettingsBuilderRequest;
import org.apache.maven.api.services.SettingsBuilderResult;
import org.apache.maven.api.services.Source;
import org.apache.maven.api.services.xml.SettingsXmlFactory;
import org.apache.maven.api.services.xml.XmlReaderException;
import org.apache.maven.api.services.xml.XmlReaderRequest;
import org.apache.maven.api.settings.Activation;
import org.apache.maven.api.settings.IdentifiableBase;
import org.apache.maven.api.settings.Profile;
import org.apache.maven.api.settings.Repository;
import org.apache.maven.api.settings.RepositoryPolicy;
import org.apache.maven.api.settings.Server;
import org.apache.maven.api.settings.Settings;
import org.apache.maven.settings.v4.SettingsMerger;
import org.apache.maven.settings.v4.SettingsTransformer;
import org.codehaus.plexus.components.secdispatcher.Dispatcher;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.codehaus.plexus.components.secdispatcher.internal.DefaultSecDispatcher;
/**
* Builds the effective settings from a user settings file and/or a global settings file.
*
*/
@Named
public class DefaultSettingsBuilder implements SettingsBuilder {
private final DefaultSettingsValidator settingsValidator = new DefaultSettingsValidator();
private final SettingsMerger settingsMerger = new SettingsMerger() {
@Override
protected KeyComputer<Repository> getRepositoryKey() {
return IdentifiableBase::getId;
}
};
private final SettingsXmlFactory settingsXmlFactory;
private final Interpolator interpolator;
private final Map<String, Dispatcher> dispatchers;
/**
* In Maven4 the {@link SecDispatcher} is injected and build settings are fully decrypted as well.
*/
@Inject
public DefaultSettingsBuilder(
SettingsXmlFactory settingsXmlFactory, Interpolator interpolator, Map<String, Dispatcher> dispatchers) {
this.settingsXmlFactory = settingsXmlFactory;
this.interpolator = interpolator;
this.dispatchers = dispatchers;
}
@Override
public SettingsBuilderResult build(SettingsBuilderRequest request) throws SettingsBuilderException {
ProblemCollector<BuilderProblem> problems = ProblemCollector.create(request.getSession());
Source installationSource = request.getInstallationSettingsSource().orElse(null);
Settings installation = readSettings(installationSource, false, request, problems);
Source projectSource = request.getProjectSettingsSource().orElse(null);
Settings project = readSettings(projectSource, true, request, problems);
Source userSource = request.getUserSettingsSource().orElse(null);
Settings user = readSettings(userSource, false, request, problems);
Settings effective =
settingsMerger.merge(user, settingsMerger.merge(project, installation, false, null), false, null);
// If no repository is defined in the user/global settings,
// it means that we have "old" settings (as those are new in 4.0)
// so add central to the computed settings for backward compatibility.
if (effective.getRepositories().isEmpty()
&& effective.getPluginRepositories().isEmpty()) {
Repository central = Repository.newBuilder()
.id("central")
.name("Central Repository")
.url("https://repo.maven.apache.org/maven2")
.snapshots(RepositoryPolicy.newBuilder().enabled(false).build())
.build();
Repository centralWithNoUpdate = central.withReleases(
RepositoryPolicy.newBuilder().updatePolicy("never").build());
effective = Settings.newBuilder(effective)
.repositories(List.of(central))
.pluginRepositories(List.of(centralWithNoUpdate))
.build();
}
// for the special case of a drive-relative Windows path, make sure it's absolute to save plugins from trouble
String localRepository = effective.getLocalRepository();
if (localRepository != null && !localRepository.isEmpty()) {
Path file = Paths.get(localRepository);
if (!file.isAbsolute() && file.toString().startsWith(File.separator)) {
effective = effective.withLocalRepository(file.toAbsolutePath().toString());
}
}
if (problems.hasErrorProblems()) {
throw new SettingsBuilderException("Error building settings", problems);
}
return new DefaultSettingsBuilderResult(request, effective, problems);
}
private Settings readSettings(
Source settingsSource,
boolean isProjectSettings,
SettingsBuilderRequest request,
ProblemCollector<BuilderProblem> problems) {
if (settingsSource == null) {
return Settings.newInstance();
}
Settings settings;
try {
try (InputStream is = settingsSource.openStream()) {
settings = settingsXmlFactory.read(XmlReaderRequest.builder()
.inputStream(is)
.location(settingsSource.getLocation())
.strict(true)
.build());
} catch (XmlReaderException e) {
try (InputStream is = settingsSource.openStream()) {
settings = settingsXmlFactory.read(XmlReaderRequest.builder()
.inputStream(is)
.location(settingsSource.getLocation())
.strict(false)
.build());
Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
problems.reportProblem(new DefaultBuilderProblem(
settingsSource.getLocation(),
loc != null ? loc.getLineNumber() : -1,
loc != null ? loc.getColumnNumber() : -1,
e,
e.getMessage(),
BuilderProblem.Severity.WARNING));
}
}
} catch (XmlReaderException e) {
Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
problems.reportProblem(new DefaultBuilderProblem(
settingsSource.getLocation(),
loc != null ? loc.getLineNumber() : -1,
loc != null ? loc.getColumnNumber() : -1,
e,
"Non-parseable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
BuilderProblem.Severity.FATAL));
return Settings.newInstance();
} catch (IOException e) {
problems.reportProblem(new DefaultBuilderProblem(
settingsSource.getLocation(),
-1,
-1,
e,
"Non-readable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
BuilderProblem.Severity.FATAL));
return Settings.newInstance();
}
settings = interpolate(settings, request, problems);
settings = decrypt(settingsSource, settings, request, problems);
settingsValidator.validate(settings, isProjectSettings, problems);
if (isProjectSettings) {
settings = Settings.newBuilder(settings, true)
.localRepository(null)
.interactiveMode(false)
.offline(false)
.proxies(List.of())
.usePluginRegistry(false)
.servers(settings.getServers().stream()
.map(s -> Server.newBuilder(s, true)
.username(null)
.passphrase(null)
.privateKey(null)
.password(null)
.filePermissions(null)
.directoryPermissions(null)
.build())
.toList())
.build();
}
return settings;
}
private Settings interpolate(
Settings settings, SettingsBuilderRequest request, ProblemCollector<BuilderProblem> problems) {
UnaryOperator<String> src;
if (request.getInterpolationSource().isPresent()) {
src = request.getInterpolationSource().get();
} else {
Map<String, String> userProperties = request.getSession().getUserProperties();
Map<String, String> systemProperties = request.getSession().getSystemProperties();
src = Interpolator.chain(userProperties::get, systemProperties::get);
}
return new DefSettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
.visit(settings);
}
static class DefSettingsTransformer extends SettingsTransformer {
DefSettingsTransformer(UnaryOperator<String> transformer) {
super(transformer);
}
@Override
protected Activation.Builder transformActivation_Condition(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
// do not interpolate the condition activation
return builder;
}
}
private Settings decrypt(
Source settingsSource,
Settings settings,
SettingsBuilderRequest request,
ProblemCollector<BuilderProblem> problems) {
if (dispatchers.isEmpty()) {
return settings;
}
SecDispatcher secDispatcher = new DefaultSecDispatcher(dispatchers, getSecuritySettings(request.getSession()));
final AtomicInteger preMaven4Passwords = new AtomicInteger(0);
UnaryOperator<String> decryptFunction = str -> {
if (str != null && !str.isEmpty() && !str.contains("${") && secDispatcher.isAnyEncryptedString(str)) {
if (secDispatcher.isLegacyEncryptedString(str)) {
// the call above return true for too broad types of strings, original idea with 2.x sec-dispatcher
// was to make it possible to add "descriptions" to encrypted passwords. Maven 4 is
// limiting itself to decryption of ONLY the simplest cases of legacy passwords, those having form
// as documented on page: https://maven.apache.org/guides/mini/guide-encryption.html
// Examples of decrypted legacy passwords:
// <password>{COQLCE6DU6GtcS5P=}</password>
// <password>Oleg reset this password on 2009-03-11 {COQLCE6DU6GtcS5P=}</password>
// In short, secDispatcher#isLegacyEncryptedString(str) did return true, but we apply more scrutiny
// and check does string start with "{" or contains " {" (whitespace before opening curly braces),
// and that it ends with "}" strictly. Otherwise, we refuse it.
if ((!str.startsWith("{") && !str.contains(" {")) || !str.endsWith("}")) {
// this is not a legacy password we care for
return str;
}
// add a problem
preMaven4Passwords.incrementAndGet();
}
try {
return secDispatcher.decrypt(str);
} catch (Exception e) {
problems.reportProblem(new DefaultBuilderProblem(
settingsSource.getLocation(),
-1,
-1,
e,
"Could not decrypt password (fix the corrupted password or remove it, if unused) " + str,
BuilderProblem.Severity.FATAL));
}
}
return str;
};
Settings result = new SettingsTransformer(decryptFunction).visit(settings);
if (preMaven4Passwords.get() > 0) {
problems.reportProblem(new DefaultBuilderProblem(
settingsSource.getLocation(),
-1,
-1,
null,
"Detected " + preMaven4Passwords.get() + " pre-Maven 4 legacy encrypted password(s) "
+ "- configure password encryption with the help of mvnenc for increased security.",
BuilderProblem.Severity.WARNING));
}
return result;
}
private Path getSecuritySettings(ProtoSession session) {
Map<String, String> properties = session.getEffectiveProperties();
String settingsSecurity = properties.get(Constants.MAVEN_SETTINGS_SECURITY);
if (settingsSecurity != null) {
return Paths.get(settingsSecurity);
}
String mavenUserConf = properties.get(Constants.MAVEN_USER_CONF);
if (mavenUserConf != null) {
return Paths.get(mavenUserConf, Constants.MAVEN_SETTINGS_SECURITY_FILE_NAME);
}
return Paths.get(properties.get("user.home"), ".m2", Constants.MAVEN_SETTINGS_SECURITY_FILE_NAME);
}
@Override
public ProblemCollector<BuilderProblem> validate(Settings settings, boolean isProjectSettings) {
// TODO: any way to get ProtoSession here?
ProblemCollector<BuilderProblem> problems = ProblemCollector.create(null);
settingsValidator.validate(settings, isProjectSettings, problems);
return problems;
}
@Override
public Profile convert(org.apache.maven.api.model.Profile profile) {
return SettingsUtilsV4.convertToSettingsProfile(profile);
}
@Override
public org.apache.maven.api.model.Profile convert(Profile profile) {
return SettingsUtilsV4.convertFromSettingsProfile(profile);
}
/**
* Collects the output of the settings builder.
*
*/
static class DefaultSettingsBuilderResult implements SettingsBuilderResult {
private final SettingsBuilderRequest request;
private final Settings effectiveSettings;
private final ProblemCollector<BuilderProblem> problems;
DefaultSettingsBuilderResult(
SettingsBuilderRequest request, Settings effectiveSettings, ProblemCollector<BuilderProblem> problems) {
this.request = request;
this.effectiveSettings = effectiveSettings;
this.problems = (problems != null) ? problems : ProblemCollector.empty();
}
@Override
public SettingsBuilderRequest getRequest() {
return request;
}
@Override
public Settings getEffectiveSettings() {
return effectiveSettings;
}
@Override
public ProblemCollector<BuilderProblem> getProblems() {
return problems;
}
}
}