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.internal.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.ArrayList;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.concurrent.atomic.AtomicInteger;
33  import java.util.function.Function;
34  import java.util.function.Supplier;
35  
36  import org.apache.maven.api.Constants;
37  import org.apache.maven.api.ProtoSession;
38  import org.apache.maven.api.di.Inject;
39  import org.apache.maven.api.di.Named;
40  import org.apache.maven.api.services.BuilderProblem;
41  import org.apache.maven.api.services.Interpolator;
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.Profile;
52  import org.apache.maven.api.settings.Repository;
53  import org.apache.maven.api.settings.RepositoryPolicy;
54  import org.apache.maven.api.settings.Server;
55  import org.apache.maven.api.settings.Settings;
56  import org.apache.maven.internal.impl.model.DefaultInterpolator;
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  
74      private final SettingsXmlFactory settingsXmlFactory;
75  
76      private final Interpolator interpolator;
77  
78      private final Map<String, Dispatcher> dispatchers;
79  
80      /**
81       * This ctor is used in legacy components.
82       */
83      public DefaultSettingsBuilder() {
84          this(new DefaultSettingsXmlFactory(), new DefaultInterpolator(), Map.of());
85      }
86  
87      /**
88       * In Maven4 the {@link SecDispatcher} is injected and build settings are fully decrypted as well.
89       */
90      @Inject
91      public DefaultSettingsBuilder(
92              SettingsXmlFactory settingsXmlFactory, Interpolator interpolator, Map<String, Dispatcher> dispatchers) {
93          this.settingsXmlFactory = settingsXmlFactory;
94          this.interpolator = interpolator;
95          this.dispatchers = dispatchers;
96      }
97  
98      @Override
99      public SettingsBuilderResult build(SettingsBuilderRequest request) throws SettingsBuilderException {
100         List<BuilderProblem> problems = new ArrayList<>();
101 
102         Source installationSource = request.getInstallationSettingsSource().orElse(null);
103         Settings installation = readSettings(installationSource, false, request, problems);
104 
105         Source projectSource = request.getProjectSettingsSource().orElse(null);
106         Settings project = readSettings(projectSource, true, request, problems);
107 
108         Source userSource = request.getUserSettingsSource().orElse(null);
109         Settings user = readSettings(userSource, false, request, problems);
110 
111         Settings effective =
112                 settingsMerger.merge(user, settingsMerger.merge(project, installation, false, null), false, null);
113 
114         // If no repository is defined in the user/global settings,
115         // it means that we have "old" settings (as those are new in 4.0)
116         // so add central to the computed settings for backward compatibility.
117         if (effective.getRepositories().isEmpty()
118                 && effective.getPluginRepositories().isEmpty()) {
119             Repository central = Repository.newBuilder()
120                     .id("central")
121                     .name("Central Repository")
122                     .url("https://repo.maven.apache.org/maven2")
123                     .snapshots(RepositoryPolicy.newBuilder().enabled(false).build())
124                     .build();
125             Repository centralWithNoUpdate = central.withReleases(
126                     RepositoryPolicy.newBuilder().updatePolicy("never").build());
127             effective = Settings.newBuilder(effective)
128                     .repositories(List.of(central))
129                     .pluginRepositories(List.of(centralWithNoUpdate))
130                     .build();
131         }
132 
133         // for the special case of a drive-relative Windows path, make sure it's absolute to save plugins from trouble
134         String localRepository = effective.getLocalRepository();
135         if (localRepository != null && !localRepository.isEmpty()) {
136             Path file = Paths.get(localRepository);
137             if (!file.isAbsolute() && file.toString().startsWith(File.separator)) {
138                 effective = effective.withLocalRepository(file.toAbsolutePath().toString());
139             }
140         }
141 
142         if (hasErrors(problems)) {
143             throw new SettingsBuilderException("Error building settings", problems);
144         }
145 
146         return new DefaultSettingsBuilderResult(effective, problems);
147     }
148 
149     private boolean hasErrors(List<BuilderProblem> problems) {
150         if (problems != null) {
151             for (BuilderProblem problem : problems) {
152                 if (BuilderProblem.Severity.ERROR.compareTo(problem.getSeverity()) >= 0) {
153                     return true;
154                 }
155             }
156         }
157 
158         return false;
159     }
160 
161     private Settings readSettings(
162             Source settingsSource,
163             boolean isProjectSettings,
164             SettingsBuilderRequest request,
165             List<BuilderProblem> problems) {
166         if (settingsSource == null) {
167             return Settings.newInstance();
168         }
169 
170         Settings settings;
171 
172         try {
173             try (InputStream is = settingsSource.openStream()) {
174                 settings = settingsXmlFactory.read(XmlReaderRequest.builder()
175                         .inputStream(is)
176                         .location(settingsSource.getLocation())
177                         .strict(true)
178                         .build());
179             } catch (XmlReaderException e) {
180                 try (InputStream is = settingsSource.openStream()) {
181                     settings = settingsXmlFactory.read(XmlReaderRequest.builder()
182                             .inputStream(is)
183                             .location(settingsSource.getLocation())
184                             .strict(false)
185                             .build());
186                     Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
187                     problems.add(new DefaultBuilderProblem(
188                             settingsSource.getLocation(),
189                             loc != null ? loc.getLineNumber() : -1,
190                             loc != null ? loc.getColumnNumber() : -1,
191                             e,
192                             e.getMessage(),
193                             BuilderProblem.Severity.WARNING));
194                 }
195             }
196         } catch (XmlReaderException e) {
197             Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
198             problems.add(new DefaultBuilderProblem(
199                     settingsSource.getLocation(),
200                     loc != null ? loc.getLineNumber() : -1,
201                     loc != null ? loc.getColumnNumber() : -1,
202                     e,
203                     "Non-parseable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
204                     BuilderProblem.Severity.FATAL));
205             return Settings.newInstance();
206         } catch (IOException e) {
207             problems.add(new DefaultBuilderProblem(
208                     settingsSource.getLocation(),
209                     -1,
210                     -1,
211                     e,
212                     "Non-readable settings " + settingsSource.getLocation() + ": " + e.getMessage(),
213                     BuilderProblem.Severity.FATAL));
214             return Settings.newInstance();
215         }
216 
217         settings = interpolate(settings, request, problems);
218         settings = decrypt(settingsSource, settings, request, problems);
219 
220         settingsValidator.validate(settings, isProjectSettings, problems);
221 
222         if (isProjectSettings) {
223             settings = Settings.newBuilder(settings, true)
224                     .localRepository(null)
225                     .interactiveMode(false)
226                     .offline(false)
227                     .proxies(List.of())
228                     .usePluginRegistry(false)
229                     .servers(settings.getServers().stream()
230                             .map(s -> Server.newBuilder(s, true)
231                                     .username(null)
232                                     .passphrase(null)
233                                     .privateKey(null)
234                                     .password(null)
235                                     .filePermissions(null)
236                                     .directoryPermissions(null)
237                                     .build())
238                             .toList())
239                     .build();
240         }
241 
242         return settings;
243     }
244 
245     private Settings interpolate(Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) {
246         Function<String, String> src;
247         if (request.getInterpolationSource().isPresent()) {
248             src = request.getInterpolationSource().get();
249         } else {
250             Map<String, String> userProperties = request.getSession().getUserProperties();
251             Map<String, String> systemProperties = request.getSession().getSystemProperties();
252             src = Interpolator.chain(userProperties::get, systemProperties::get);
253         }
254         return new DefSettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
255                 .visit(settings);
256     }
257 
258     static class DefSettingsTransformer extends SettingsTransformer {
259         DefSettingsTransformer(Function<String, String> transformer) {
260             super(transformer);
261         }
262 
263         @Override
264         protected Activation.Builder transformActivation_Condition(
265                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
266             // do not interpolate the condition activation
267             return builder;
268         }
269     }
270 
271     private Settings decrypt(
272             Source settingsSource, Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) {
273         if (dispatchers.isEmpty()) {
274             return settings;
275         }
276         SecDispatcher secDispatcher = new DefaultSecDispatcher(dispatchers, getSecuritySettings(request.getSession()));
277         final AtomicInteger preMaven4Passwords = new AtomicInteger(0);
278         Function<String, String> decryptFunction = str -> {
279             if (str != null && !str.isEmpty() && !str.contains("${") && secDispatcher.isAnyEncryptedString(str)) {
280                 if (secDispatcher.isLegacyEncryptedString(str)) {
281                     // add a problem
282                     preMaven4Passwords.incrementAndGet();
283                 }
284                 try {
285                     return secDispatcher.decrypt(str);
286                 } catch (Exception e) {
287                     problems.add(new DefaultBuilderProblem(
288                             settingsSource.getLocation(),
289                             -1,
290                             -1,
291                             e,
292                             "Could not decrypt password (fix the corrupted password or remove it, if unused) " + str,
293                             BuilderProblem.Severity.FATAL));
294                 }
295             }
296             return str;
297         };
298         Settings result = new SettingsTransformer(decryptFunction).visit(settings);
299         if (preMaven4Passwords.get() > 0) {
300             problems.add(new DefaultBuilderProblem(
301                     settingsSource.getLocation(),
302                     -1,
303                     -1,
304                     null,
305                     "Detected " + preMaven4Passwords.get() + " pre-Maven 4 legacy encrypted password(s) "
306                             + "- configure password encryption with the help of mvnenc for increased security.",
307                     BuilderProblem.Severity.WARNING));
308         }
309         return result;
310     }
311 
312     private Path getSecuritySettings(ProtoSession session) {
313         Map<String, String> properties = session.getUserProperties();
314         String settingsSecurity = properties.get(Constants.MAVEN_SETTINGS_SECURITY);
315         if (settingsSecurity != null) {
316             return Paths.get(settingsSecurity);
317         }
318         String mavenUserConf = properties.get(Constants.MAVEN_USER_CONF);
319         if (mavenUserConf != null) {
320             return Paths.get(mavenUserConf, Constants.MAVEN_SETTINGS_SECURITY_FILE_NAME);
321         }
322         return Paths.get(properties.get("user.home"), ".m2", Constants.MAVEN_SETTINGS_SECURITY_FILE_NAME);
323     }
324 
325     @Override
326     public List<BuilderProblem> validate(Settings settings, boolean isProjectSettings) {
327         ArrayList<BuilderProblem> problems = new ArrayList<>();
328         settingsValidator.validate(settings, isProjectSettings, problems);
329         return problems;
330     }
331 
332     @Override
333     public Profile convert(org.apache.maven.api.model.Profile profile) {
334         return SettingsUtilsV4.convertToSettingsProfile(profile);
335     }
336 
337     @Override
338     public org.apache.maven.api.model.Profile convert(Profile profile) {
339         return SettingsUtilsV4.convertFromSettingsProfile(profile);
340     }
341 
342     /**
343      * Collects the output of the settings builder.
344      *
345      */
346     static class DefaultSettingsBuilderResult implements SettingsBuilderResult {
347 
348         private final Settings effectiveSettings;
349 
350         private final List<BuilderProblem> problems;
351 
352         DefaultSettingsBuilderResult(Settings effectiveSettings, List<BuilderProblem> problems) {
353             this.effectiveSettings = effectiveSettings;
354             this.problems = (problems != null) ? problems : new ArrayList<>();
355         }
356 
357         @Override
358         public Settings getEffectiveSettings() {
359             return effectiveSettings;
360         }
361 
362         @Override
363         public List<BuilderProblem> getProblems() {
364             return problems;
365         }
366     }
367 }