View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.impl;
20  
21  import javax.xml.stream.Location;
22  import javax.xml.stream.XMLStreamException;
23  
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import java.util.function.Supplier;
33  import java.util.function.UnaryOperator;
34  
35  import org.apache.maven.api.Constants;
36  import org.apache.maven.api.ProtoSession;
37  import org.apache.maven.api.di.Inject;
38  import org.apache.maven.api.di.Named;
39  import org.apache.maven.api.services.BuilderProblem;
40  import org.apache.maven.api.services.Interpolator;
41  import org.apache.maven.api.services.ProblemCollector;
42  import org.apache.maven.api.services.SettingsBuilder;
43  import org.apache.maven.api.services.SettingsBuilderException;
44  import org.apache.maven.api.services.SettingsBuilderRequest;
45  import org.apache.maven.api.services.SettingsBuilderResult;
46  import org.apache.maven.api.services.Source;
47  import org.apache.maven.api.services.xml.SettingsXmlFactory;
48  import org.apache.maven.api.services.xml.XmlReaderException;
49  import org.apache.maven.api.services.xml.XmlReaderRequest;
50  import org.apache.maven.api.settings.Activation;
51  import org.apache.maven.api.settings.IdentifiableBase;
52  import org.apache.maven.api.settings.Profile;
53  import org.apache.maven.api.settings.Repository;
54  import org.apache.maven.api.settings.RepositoryPolicy;
55  import org.apache.maven.api.settings.Server;
56  import org.apache.maven.api.settings.Settings;
57  import org.apache.maven.settings.v4.SettingsMerger;
58  import org.apache.maven.settings.v4.SettingsTransformer;
59  import org.codehaus.plexus.components.secdispatcher.Dispatcher;
60  import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
61  import org.codehaus.plexus.components.secdispatcher.internal.DefaultSecDispatcher;
62  
63  /**
64   * Builds the effective settings from a user settings file and/or a global settings file.
65   *
66   */
67  @Named
68  public class DefaultSettingsBuilder implements SettingsBuilder {
69  
70      private final DefaultSettingsValidator settingsValidator = new DefaultSettingsValidator();
71  
72      private final SettingsMerger settingsMerger = new SettingsMerger() {
73          @Override
74          protected KeyComputer<Repository> getRepositoryKey() {
75              return IdentifiableBase::getId;
76          }
77      };
78  
79      private final SettingsXmlFactory settingsXmlFactory;
80  
81      private final Interpolator interpolator;
82  
83      private final Map<String, Dispatcher> dispatchers;
84  
85      /**
86       * In Maven4 the {@link SecDispatcher} is injected and build settings are fully decrypted as well.
87       */
88      @Inject
89      public DefaultSettingsBuilder(
90              SettingsXmlFactory settingsXmlFactory, Interpolator interpolator, Map<String, Dispatcher> dispatchers) {
91          this.settingsXmlFactory = settingsXmlFactory;
92          this.interpolator = interpolator;
93          this.dispatchers = dispatchers;
94      }
95  
96      @Override
97      public SettingsBuilderResult build(SettingsBuilderRequest request) throws SettingsBuilderException {
98          ProblemCollector<BuilderProblem> problems = ProblemCollector.create(request.getSession());
99  
100         Source installationSource = request.getInstallationSettingsSource().orElse(null);
101         Settings installation = readSettings(installationSource, false, request, problems);
102 
103         Source projectSource = request.getProjectSettingsSource().orElse(null);
104         Settings project = readSettings(projectSource, true, request, problems);
105 
106         Source userSource = request.getUserSettingsSource().orElse(null);
107         Settings user = readSettings(userSource, false, request, problems);
108 
109         Settings effective =
110                 settingsMerger.merge(user, settingsMerger.merge(project, installation, false, null), false, null);
111 
112         // If no repository is defined in the user/global settings,
113         // it means that we have "old" settings (as those are new in 4.0)
114         // so add central to the computed settings for backward compatibility.
115         if (effective.getRepositories().isEmpty()
116                 && effective.getPluginRepositories().isEmpty()) {
117             Repository central = Repository.newBuilder()
118                     .id("central")
119                     .name("Central Repository")
120                     .url("https://repo.maven.apache.org/maven2")
121                     .snapshots(RepositoryPolicy.newBuilder().enabled(false).build())
122                     .build();
123             Repository centralWithNoUpdate = central.withReleases(
124                     RepositoryPolicy.newBuilder().updatePolicy("never").build());
125             effective = Settings.newBuilder(effective)
126                     .repositories(List.of(central))
127                     .pluginRepositories(List.of(centralWithNoUpdate))
128                     .build();
129         }
130 
131         // for the special case of a drive-relative Windows path, make sure it's absolute to save plugins from trouble
132         String localRepository = effective.getLocalRepository();
133         if (localRepository != null && !localRepository.isEmpty()) {
134             Path file = Paths.get(localRepository);
135             if (!file.isAbsolute() && file.toString().startsWith(File.separator)) {
136                 effective = effective.withLocalRepository(file.toAbsolutePath().toString());
137             }
138         }
139 
140         if (problems.hasErrorProblems()) {
141             throw new SettingsBuilderException("Error building settings", problems);
142         }
143 
144         return new DefaultSettingsBuilderResult(request, effective, problems);
145     }
146 
147     private Settings readSettings(
148             Source settingsSource,
149             boolean isProjectSettings,
150             SettingsBuilderRequest request,
151             ProblemCollector<BuilderProblem> problems) {
152         if (settingsSource == null) {
153             return Settings.newInstance();
154         }
155 
156         Settings settings;
157 
158         try {
159             try (InputStream is = settingsSource.openStream()) {
160                 settings = settingsXmlFactory.read(XmlReaderRequest.builder()
161                         .inputStream(is)
162                         .location(settingsSource.getLocation())
163                         .strict(true)
164                         .build());
165             } catch (XmlReaderException e) {
166                 try (InputStream is = settingsSource.openStream()) {
167                     settings = settingsXmlFactory.read(XmlReaderRequest.builder()
168                             .inputStream(is)
169                             .location(settingsSource.getLocation())
170                             .strict(false)
171                             .build());
172                     Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
173                     problems.reportProblem(new DefaultBuilderProblem(
174                             settingsSource.getLocation(),
175                             loc != null ? loc.getLineNumber() : -1,
176                             loc != null ? loc.getColumnNumber() : -1,
177                             e,
178                             e.getMessage(),
179                             BuilderProblem.Severity.WARNING));
180                 }
181             }
182         } catch (XmlReaderException e) {
183             Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
184             problems.reportProblem(new DefaultBuilderProblem(
185                     settingsSource.getLocation(),
186                     loc != null ? loc.getLineNumber() : -1,
187                     loc != null ? loc.getColumnNumber() : -1,
188                     e,
189                     "Non-parseable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
190                     BuilderProblem.Severity.FATAL));
191             return Settings.newInstance();
192         } catch (IOException e) {
193             problems.reportProblem(new DefaultBuilderProblem(
194                     settingsSource.getLocation(),
195                     -1,
196                     -1,
197                     e,
198                     "Non-readable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
199                     BuilderProblem.Severity.FATAL));
200             return Settings.newInstance();
201         }
202 
203         settings = interpolate(settings, request, problems);
204         settings = decrypt(settingsSource, settings, request, problems);
205 
206         settingsValidator.validate(settings, isProjectSettings, problems);
207 
208         if (isProjectSettings) {
209             settings = Settings.newBuilder(settings, true)
210                     .localRepository(null)
211                     .interactiveMode(false)
212                     .offline(false)
213                     .proxies(List.of())
214                     .usePluginRegistry(false)
215                     .servers(settings.getServers().stream()
216                             .map(s -> Server.newBuilder(s, true)
217                                     .username(null)
218                                     .passphrase(null)
219                                     .privateKey(null)
220                                     .password(null)
221                                     .filePermissions(null)
222                                     .directoryPermissions(null)
223                                     .build())
224                             .toList())
225                     .build();
226         }
227 
228         return settings;
229     }
230 
231     private Settings interpolate(
232             Settings settings, SettingsBuilderRequest request, ProblemCollector<BuilderProblem> problems) {
233         UnaryOperator<String> src;
234         if (request.getInterpolationSource().isPresent()) {
235             src = request.getInterpolationSource().get();
236         } else {
237             Map<String, String> userProperties = request.getSession().getUserProperties();
238             Map<String, String> systemProperties = request.getSession().getSystemProperties();
239             src = Interpolator.chain(userProperties::get, systemProperties::get);
240         }
241         return new DefSettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
242                 .visit(settings);
243     }
244 
245     static class DefSettingsTransformer extends SettingsTransformer {
246         DefSettingsTransformer(UnaryOperator<String> transformer) {
247             super(transformer);
248         }
249 
250         @Override
251         protected Activation.Builder transformActivation_Condition(
252                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
253             // do not interpolate the condition activation
254             return builder;
255         }
256     }
257 
258     private Settings decrypt(
259             Source settingsSource,
260             Settings settings,
261             SettingsBuilderRequest request,
262             ProblemCollector<BuilderProblem> problems) {
263         if (dispatchers.isEmpty()) {
264             return settings;
265         }
266         SecDispatcher secDispatcher = new DefaultSecDispatcher(dispatchers, getSecuritySettings(request.getSession()));
267         final AtomicInteger preMaven4Passwords = new AtomicInteger(0);
268         UnaryOperator<String> decryptFunction = str -> {
269             if (str != null && !str.isEmpty() && !str.contains("${") && secDispatcher.isAnyEncryptedString(str)) {
270                 if (secDispatcher.isLegacyEncryptedString(str)) {
271                     // add a problem
272                     preMaven4Passwords.incrementAndGet();
273                 }
274                 try {
275                     return secDispatcher.decrypt(str);
276                 } catch (Exception e) {
277                     problems.reportProblem(new DefaultBuilderProblem(
278                             settingsSource.getLocation(),
279                             -1,
280                             -1,
281                             e,
282                             "Could not decrypt password (fix the corrupted password or remove it, if unused) " + str,
283                             BuilderProblem.Severity.FATAL));
284                 }
285             }
286             return str;
287         };
288         Settings result = new SettingsTransformer(decryptFunction).visit(settings);
289         if (preMaven4Passwords.get() > 0) {
290             problems.reportProblem(new DefaultBuilderProblem(
291                     settingsSource.getLocation(),
292                     -1,
293                     -1,
294                     null,
295                     "Detected " + preMaven4Passwords.get() + " pre-Maven 4 legacy encrypted password(s) "
296                             + "- configure password encryption with the help of mvnenc for increased security.",
297                     BuilderProblem.Severity.WARNING));
298         }
299         return result;
300     }
301 
302     private Path getSecuritySettings(ProtoSession session) {
303         Map<String, String> properties = session.getUserProperties();
304         String settingsSecurity = properties.get(Constants.MAVEN_SETTINGS_SECURITY);
305         if (settingsSecurity != null) {
306             return Paths.get(settingsSecurity);
307         }
308         String mavenUserConf = properties.get(Constants.MAVEN_USER_CONF);
309         if (mavenUserConf != null) {
310             return Paths.get(mavenUserConf, Constants.MAVEN_SETTINGS_SECURITY_FILE_NAME);
311         }
312         return Paths.get(properties.get("user.home"), ".m2", Constants.MAVEN_SETTINGS_SECURITY_FILE_NAME);
313     }
314 
315     @Override
316     public ProblemCollector<BuilderProblem> validate(Settings settings, boolean isProjectSettings) {
317         // TODO: any way to get ProtoSession here?
318         ProblemCollector<BuilderProblem> problems = ProblemCollector.create(null);
319         settingsValidator.validate(settings, isProjectSettings, problems);
320         return problems;
321     }
322 
323     @Override
324     public Profile convert(org.apache.maven.api.model.Profile profile) {
325         return SettingsUtilsV4.convertToSettingsProfile(profile);
326     }
327 
328     @Override
329     public org.apache.maven.api.model.Profile convert(Profile profile) {
330         return SettingsUtilsV4.convertFromSettingsProfile(profile);
331     }
332 
333     /**
334      * Collects the output of the settings builder.
335      *
336      */
337     static class DefaultSettingsBuilderResult implements SettingsBuilderResult {
338 
339         private final SettingsBuilderRequest request;
340 
341         private final Settings effectiveSettings;
342 
343         private final ProblemCollector<BuilderProblem> problems;
344 
345         DefaultSettingsBuilderResult(
346                 SettingsBuilderRequest request, Settings effectiveSettings, ProblemCollector<BuilderProblem> problems) {
347             this.request = request;
348             this.effectiveSettings = effectiveSettings;
349             this.problems = (problems != null) ? problems : ProblemCollector.empty();
350         }
351 
352         @Override
353         public SettingsBuilderRequest getRequest() {
354             return request;
355         }
356 
357         @Override
358         public Settings getEffectiveSettings() {
359             return effectiveSettings;
360         }
361 
362         @Override
363         public ProblemCollector<BuilderProblem> getProblems() {
364             return problems;
365         }
366     }
367 }