diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java index 50e5777de2fa..4c2f2d65ea8b 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java @@ -46,8 +46,6 @@ import org.apache.maven.api.settings.Settings; import org.apache.maven.cling.invoker.mvnup.UpgradeContext; import org.apache.maven.impl.standalone.ApiRunner; -import org.codehaus.plexus.components.secdispatcher.Dispatcher; -import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; import org.eclipse.aether.spi.connector.transport.TransporterFactory; import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor; import org.eclipse.aether.spi.io.PathProcessor; @@ -257,18 +255,9 @@ protected static String remoteResolutionUnsupportedReason(UpgradeContext context } private Session createMaven4Session(UpgradeContext context) { - // Reuse the operator's real user home (settings.xml) and local repository so remote - // resolution follows the configured repository posture instead of the standalone - // test defaults. When settings were never loaded (unit tests, embedding), keep the - // isolated test user home. - boolean settingsLoaded = context.effectiveSettings != null; - Session session = ApiRunner.createSession( - injector -> { - injector.bindInstance(Dispatcher.class, new LegacyDispatcher()); - injector.bindImplicit(TransporterFactoryConfig.class); - }, - context.localRepositoryPath, - !settingsLoaded); + Session session = ApiRunner.createSession(injector -> { + injector.bindImplicit(TransporterFactoryConfig.class); + }); if (remoteResolutionUnsupportedReason(context) != null) { // No remote repositories at all, rather than resolving from hardcoded public diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java index 5620c4c4268e..97a8853cd7b8 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java @@ -18,7 +18,6 @@ */ package org.apache.maven.impl.resolver; -import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayList; diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java index 25740a0ff506..24f6b3488bc1 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java @@ -281,8 +281,6 @@ private Versioning readVersions( return (versioning != null) ? versioning : Versioning.newInstance(); } - - private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java index b6a0a711e817..f18d9f09a515 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java @@ -80,7 +80,7 @@ public MavenSessionBuilderSupplier(RepositorySystem repositorySystem, boolean ma : Maven4ScopeManagerConfiguration.INSTANCE); } - protected DependencyTraverser getDependencyTraverser() { + public DependencyTraverser getDependencyTraverser() { return new FatArtifactTraverser(); } @@ -99,7 +99,7 @@ public DependencyManager getDependencyManager(boolean transitive) { return new ClassicDependencyManager(getScopeManager()); } - protected DependencySelector getDependencySelector() { + public DependencySelector getDependencySelector() { return new AndDependencySelector( ScopeDependencySelector.legacy( null, Arrays.asList(DependencyScope.TEST.id(), DependencyScope.PROVIDED.id())), @@ -107,7 +107,7 @@ protected DependencySelector getDependencySelector() { new ExclusionDependencySelector()); } - protected DependencyGraphTransformer getDependencyGraphTransformer() { + public DependencyGraphTransformer getDependencyGraphTransformer() { return new ChainedDependencyGraphTransformer( new TypeCollector(), new ConflictResolver( @@ -126,13 +126,13 @@ protected DependencyGraphTransformer getDependencyGraphTransformer() { *

* Important: this "static" list of types should be in-sync with core provided types. */ - protected ArtifactTypeRegistry getArtifactTypeRegistry() { + public ArtifactTypeRegistry getArtifactTypeRegistry() { DefaultArtifactTypeRegistry stereotypes = new DefaultArtifactTypeRegistry(); new DefaultTypeProvider().types().forEach(t -> stereotypes.add(t.toArtifactType())); return stereotypes; } - protected ArtifactDescriptorPolicy getArtifactDescriptorPolicy() { + public ArtifactDescriptorPolicy getArtifactDescriptorPolicy() { return new SimpleArtifactDescriptorPolicy(true, true); } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java index 3421eadf1bf6..b64dafa86944 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java @@ -18,6 +18,9 @@ */ package org.apache.maven.impl.standalone; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; @@ -29,11 +32,14 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.maven.api.Artifact; +import org.apache.maven.api.Constants; import org.apache.maven.api.Lifecycle; import org.apache.maven.api.MonotonicClock; import org.apache.maven.api.Packaging; @@ -45,6 +51,7 @@ import org.apache.maven.api.Version; import org.apache.maven.api.annotations.Nonnull; import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Provides; import org.apache.maven.api.di.SessionScoped; import org.apache.maven.api.model.PluginContainer; @@ -57,32 +64,69 @@ import org.apache.maven.api.services.RepositoryFactory; import org.apache.maven.api.services.SettingsBuilder; import org.apache.maven.api.services.TypeRegistry; +import org.apache.maven.api.services.VersionParser; +import org.apache.maven.api.settings.Mirror; +import org.apache.maven.api.settings.Proxy; +import org.apache.maven.api.settings.Server; import org.apache.maven.api.settings.Settings; import org.apache.maven.api.spi.TypeProvider; import org.apache.maven.api.toolchain.ToolchainModel; import org.apache.maven.di.Injector; import org.apache.maven.di.Key; import org.apache.maven.di.impl.DIException; +import org.apache.maven.di.impl.InjectorImpl; import org.apache.maven.impl.AbstractSession; import org.apache.maven.impl.InternalSession; import org.apache.maven.impl.di.SessionScope; +import org.apache.maven.impl.model.DefaultInterpolator; +import org.apache.maven.impl.resolver.MavenSessionBuilderSupplier; import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration; +import org.codehaus.plexus.components.secdispatcher.Cipher; +import org.codehaus.plexus.components.secdispatcher.Dispatcher; +import org.codehaus.plexus.components.secdispatcher.MasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.cipher.AESGCMNoPadding; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher; +import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.PinEntryMasterSource; +import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.internal.impl.scope.ScopeManagerImpl; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; +import org.eclipse.aether.util.repository.DefaultMirrorSelector; +import org.eclipse.aether.util.repository.DefaultProxySelector; /** * Provides functionality for running Maven API in a standalone mode. *

* This class serves as the main entry point for executing Maven operations outside * of the standard Maven build environment. It provides methods for creating and - * managing Maven sessions in a simplified context, primarily for testing and - * specialized execution scenarios. + * managing Maven sessions in a simplified context, suitable for tools, IDE integrations, + * and specialized execution scenarios. *

* + *

The standalone session reads and applies the user's {@code settings.xml} including:

+ * + * + *

It also loads {@code maven-system.properties} from the Maven configuration directory + * ({@code ${maven.home}/conf} or as configured via {@code maven.installation.conf}/{@code maven.conf}) + * and from the user's Maven home ({@code ~/.m2/}), merging both into the session's system properties. + * User properties are loaded from {@code ~/.m2/maven-user.properties} and exposed via + * {@link Session#getUserProperties()}.

+ * *

Example usage:

*
  * Session session = ApiRunner.createSession();
@@ -99,6 +143,27 @@
  */
 public class ApiRunner {
 
+    /**
+     * Controls how the standalone session handles settings encryption/decryption.
+     *
+     * 

Maven settings ({@code settings.xml}) may contain encrypted server passwords. + * Decryption requires the {@code plexus-sec-dispatcher} library on the classpath. + * This enum lets callers control the behavior when dispatchers are (or are not) available.

+ */ + public enum SecurityMode { + /** Do not attempt to configure security dispatchers. Encrypted passwords are passed through as-is. */ + NONE, + /** Try to configure security dispatchers; silently skip if the required classes are not on the classpath. */ + IF_AVAILABLE, + /** + * Try to configure security dispatchers; warn to {@code System.err} if the required classes + * are not on the classpath. This is the default. + */ + IF_AVAILABLE_WARN, + /** Security dispatchers are required; throw {@link MavenException} if they cannot be configured. */ + REQUIRED + } + /** * Creates a new Maven session with default configuration. * @@ -126,29 +191,27 @@ public static Session createSession(Consumer injectorConsumer) { * @return a new {@link Session} instance */ public static Session createSession(Consumer injectorConsumer, Path localRepo) { - return createSession(injectorConsumer, localRepo, true); + return createSession(injectorConsumer, localRepo, SecurityMode.IF_AVAILABLE_WARN); } /** - * Creates a new Maven session with custom injector configuration, local repository path - * and control over {@code user.home} isolation. + * Creates a new Maven session with custom injector configuration, local repository path, + * and security mode. * * @param injectorConsumer consumer function to customize the injector * @param localRepo path to the local repository - * @param isolateUserHome when {@code true}, the session remaps {@code user.home} to the - * {@code target} directory so the invoking user's {@code settings.xml} and local - * repository cannot interfere with tests; callers that want the operator's real - * configuration honored (such as the mvnup tool) must pass {@code false} + * @param securityMode controls how encrypted passwords in settings are handled * @return a new {@link Session} instance */ - public static Session createSession(Consumer injectorConsumer, Path localRepo, boolean isolateUserHome) { + public static Session createSession( + Consumer injectorConsumer, Path localRepo, SecurityMode securityMode) { Injector injector = Injector.create(); injector.bindInstance(Injector.class, injector); injector.bindImplicit(ApiRunner.class); injector.bindImplicit(RepositorySystemSupplier.class); injector.bindInstance(LocalRepoProvider.class, () -> localRepo); - injector.bindInstance(UserHomeIsolation.class, () -> isolateUserHome); injector.discover(ApiRunner.class.getClassLoader()); + configureSecurityDispatchers(injector, securityMode != null ? securityMode : SecurityMode.IF_AVAILABLE_WARN); if (injectorConsumer != null) { injectorConsumer.accept(injector); } @@ -161,14 +224,45 @@ public static Session createSession(Consumer injectorConsumer, Path lo } /** - * Controls whether the standalone session isolates {@code user.home} from the invoking - * user's environment. + * Attempts to bind security dispatcher classes for settings password decryption. + * The dispatchers (legacy and master) are loaded from the {@code plexus-sec-dispatcher} + * library. If the library is not on the classpath, the behavior depends on the + * {@link SecurityMode}. + * + *

If dispatchers are already bound (e.g., via {@code discover()} in tests or by a + * custom {@code injectorConsumer}), this method is a no-op to avoid duplicate bindings.

*/ - interface UserHomeIsolation { - /** - * @return {@code true} to remap {@code user.home} away from the real user home - */ - boolean isolated(); + private static void configureSecurityDispatchers(Injector injector, SecurityMode mode) { + if (mode == SecurityMode.NONE) { + return; + } + try { + Class.forName("org.codehaus.plexus.components.secdispatcher.Dispatcher"); + // Skip if dispatchers are already bound (e.g., from discover() scanning test classes) + if (!hasExistingDispatchers(injector)) { + injector.bindImplicit(SecDispatcherBindings.class); + } + } catch (ClassNotFoundException | NoClassDefFoundError e) { + switch (mode) { + case REQUIRED: + throw new MavenException( + "Security dispatchers required but plexus-sec-dispatcher is not on the classpath", e); + case IF_AVAILABLE_WARN: + System.err.println("WARNING: plexus-sec-dispatcher not available on classpath; " + + "encrypted passwords in settings.xml will not be decrypted"); + break; + default: + break; + } + } + } + + private static boolean hasExistingDispatchers(Injector injector) { + if (injector instanceof InjectorImpl impl) { + Set bindings = impl.getAllBindings(Dispatcher.class); + return bindings != null && !bindings.isEmpty(); + } + return false; } /** @@ -190,6 +284,9 @@ static class DefaultSession extends AbstractSession { private final Map systemProperties; private final Instant startTime = MonotonicClock.now(); + private Settings settings; + private Version mavenVersion; + private Map userProperties = Map.of(); DefaultSession(RepositorySystemSession session, RepositorySystem repositorySystem, Lookup lookup) { this(session, repositorySystem, Collections.emptyList(), null, lookup); @@ -209,12 +306,29 @@ protected DefaultSession( @Override protected Session newSession(RepositorySystemSession session, List repositories) { - return new DefaultSession(session, repositorySystem, repositories, null, lookup); + DefaultSession newSession = new DefaultSession(session, repositorySystem, repositories, null, lookup); + newSession.settings = this.settings; + newSession.mavenVersion = this.mavenVersion; + newSession.userProperties = this.userProperties; + return newSession; + } + + void setSettings(Settings settings) { + this.settings = settings; + } + + void setMavenVersion(Version mavenVersion) { + this.mavenVersion = mavenVersion; + } + + void setUserProperties(Map userProperties) { + this.userProperties = userProperties != null ? userProperties : Map.of(); } @Override + @Nonnull public Settings getSettings() { - return Settings.newInstance(); + return settings != null ? settings : Settings.newInstance(); } @Override @@ -225,7 +339,7 @@ public Collection getToolchains() { @Override public Map getUserProperties() { - return Map.of(); + return userProperties; } @Override @@ -245,7 +359,7 @@ public Map getEffectiveProperties(Project project) { @Override public Version getMavenVersion() { - return null; + return mavenVersion; } @Override @@ -393,33 +507,50 @@ public List computePhases(Lifecycle lifecycle) { @Provides @SuppressWarnings("unused") - static Session newSession( - RepositorySystem system, - Lookup lookup, - @Nullable LocalRepoProvider localRepoProvider, - @Nullable UserHomeIsolation userHomeIsolation) { + static Session newSession(RepositorySystem system, Lookup lookup, @Nullable LocalRepoProvider localRepoProvider) { Map properties = new HashMap<>(); // Env variables prefixed with "env." System.getenv().forEach((k, v) -> properties.put("env." + k, v)); // Java System properties System.getProperties().forEach((k, v) -> properties.put(k.toString(), v.toString())); - // Test isolation shim: do not let the invoking user's settings interfere with unit - // tests. Callers that want the operator's real settings.xml and local repository - // honored must create the session with isolateUserHome=false (see createSession). - if (userHomeIsolation == null || userHomeIsolation.isolated()) { - properties.put("user.home", "target"); - } - Path userHome = Paths.get(properties.get("user.home")); Path mavenUserHome = userHome.resolve(".m2"); Path mavenSystemHome = properties.containsKey("maven.home") ? Paths.get(properties.get("maven.home")) : properties.containsKey("env.MAVEN_HOME") ? Paths.get(properties.get("env.MAVEN_HOME")) : null; + // Load maven-system.properties: installation-level first (sets paths like maven.user.conf), + // then user-level (~/.m2/) for custom overrides. The installation-level file is a bootstrapper + // that in the CLI includes user and project-level files via ${includes}; since we don't support + // that directive, we load the user-level file explicitly. + Path mavenConf = resolveMavenConf(properties); + if (mavenConf != null) { + Map systemFileProps = + loadMavenProperties(mavenConf.resolve("maven-system.properties"), properties); + properties.putAll(systemFileProps); + } + properties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-system.properties"), properties)); + + // Load maven-user.properties from the user-level location (~/.m2/) only. + // The installation-level maven-user.properties (in ${maven.conf}/) contains Maven-internal + // configuration (cache config, conflict resolver) that is not appropriate for standalone use; + // it is a CLI bootstrapper that includes user and project-level files via ${includes}. + Map userProperties = new HashMap<>(); + userProperties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-user.properties"), properties)); + + // Configure the resolver session with dependency resolution machinery + MavenSessionBuilderSupplier sessionBuilderSupplier = new MavenSessionBuilderSupplier(system, false); DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false); rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE)); + rsession.setDependencyTraverser(sessionBuilderSupplier.getDependencyTraverser()); + rsession.setDependencyManager(sessionBuilderSupplier.getDependencyManager(true)); + rsession.setDependencySelector(sessionBuilderSupplier.getDependencySelector()); + rsession.setDependencyGraphTransformer(sessionBuilderSupplier.getDependencyGraphTransformer()); + rsession.setArtifactTypeRegistry(sessionBuilderSupplier.getArtifactTypeRegistry()); + rsession.setArtifactDescriptorPolicy(sessionBuilderSupplier.getArtifactDescriptorPolicy()); rsession.setSystemProperties(properties); + rsession.setUserProperties(userProperties); rsession.setConfigProperties(properties); DefaultSession session = new DefaultSession( @@ -437,6 +568,13 @@ static Session newSession( mavenUserHome.resolve("settings.xml")) .getEffectiveSettings(); + // Store the effective settings and user properties on the session + session.setSettings(settings); + session.setUserProperties(userProperties); + + // Set the Maven version + session.setMavenVersion(detectMavenVersion(lookup)); + // local repository String localRepository = settings.getLocalRepository() != null && !settings.getLocalRepository().isEmpty() @@ -446,15 +584,56 @@ static Session newSession( : mavenUserHome.resolve("repository").toString(); LocalRepositoryManager llm = system.newLocalRepositoryManager(rsession, new LocalRepository(localRepository)); rsession.setLocalRepositoryManager(llm); - // active proxies - // TODO - // active profiles - - Profile profile = session.getService(SettingsBuilder.class) - .convert(org.apache.maven.api.settings.Profile.newBuilder() - .repositories(settings.getRepositories()) - .pluginRepositories(settings.getPluginRepositories()) - .build()); + + // Apply offline mode from settings + if (settings.isOffline()) { + rsession.setOffline(true); + } + + // Apply proxy configuration from settings + DefaultProxySelector proxySelector = new DefaultProxySelector(); + for (Proxy proxy : settings.getProxies()) { + if (proxy.isActive()) { + AuthenticationBuilder authBuilder = new AuthenticationBuilder(); + authBuilder.addUsername(proxy.getUsername()).addPassword(proxy.getPassword()); + proxySelector.add( + new org.eclipse.aether.repository.Proxy( + proxy.getProtocol(), proxy.getHost(), proxy.getPort(), authBuilder.build()), + proxy.getNonProxyHosts()); + } + } + rsession.setProxySelector(proxySelector); + + // Apply mirror configuration from settings + DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector(); + for (Mirror mirror : settings.getMirrors()) { + mirrorSelector.add( + mirror.getId(), + mirror.getUrl(), + mirror.getLayout(), + false, + mirror.isBlocked(), + mirror.getMirrorOf(), + mirror.getMirrorOfLayouts()); + } + rsession.setMirrorSelector(mirrorSelector); + + // Apply server authentication from settings + DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector(); + for (Server server : settings.getServers()) { + AuthenticationBuilder authBuilder = new AuthenticationBuilder(); + authBuilder.addUsername(server.getUsername()).addPassword(server.getPassword()); + authBuilder.addPrivateKey(server.getPrivateKey(), server.getPassphrase()); + authSelector.add(server.getId(), authBuilder.build()); + } + rsession.setAuthenticationSelector(authSelector); + + // Build repositories from active profiles in settings + SettingsBuilder settingsBuilder = session.getService(SettingsBuilder.class); + Profile profile = settingsBuilder.convert(org.apache.maven.api.settings.Profile.newBuilder() + .repositories(settings.getRepositories()) + .pluginRepositories(settings.getPluginRepositories()) + .build()); RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class); List repositories = profile.getRepositories().stream() .map(repositoryFactory::createRemote) @@ -462,16 +641,164 @@ static Session newSession( InternalSession s = (InternalSession) session.withRemoteRepositories(repositories); InternalSession.associate(rsession, s); return s; + } - // List repositories = repositoryFactory.createRemote(); + /** + * Resolves the Maven configuration directory following the same lookup order as the Maven CLI: + * {@code maven.installation.conf} → {@code maven.conf} → {@code ${maven.home}/conf} → {@code ${MAVEN_HOME}/conf}. + * + * @param properties the system properties (env + Java system properties) + * @return the maven conf directory path, or {@code null} if it cannot be determined + */ + private static Path resolveMavenConf(Map properties) { + String installConf = properties.get(Constants.MAVEN_INSTALLATION_CONF); + if (installConf != null) { + return Paths.get(installConf); + } + String mavenConf = properties.get("maven.conf"); + if (mavenConf != null) { + return Paths.get(mavenConf); + } + String mavenHome = properties.get(Constants.MAVEN_HOME); + if (mavenHome != null) { + return Paths.get(mavenHome).resolve("conf"); + } + String envMavenHome = properties.get("env.MAVEN_HOME"); + if (envMavenHome != null) { + return Paths.get(envMavenHome).resolve("conf"); + } + return null; + } + + /** + * Loads properties from a file and interpolates {@code ${...}} references against the given + * fallback properties. If the file does not exist or cannot be read, an empty map is returned. + * + * @param path the properties file to load + * @param fallback fallback values for interpolation (typically system properties) + * @return the loaded and interpolated properties + */ + private static Map loadMavenProperties(Path path, Map fallback) { + if (path == null || !Files.exists(path)) { + return new HashMap<>(); + } + Properties fileProps = new Properties(); + try (InputStream is = Files.newInputStream(path)) { + fileProps.load(is); + } catch (IOException e) { + // Silently ignore: properties files are optional configuration and the standalone + // API has no logger. Failing to read a file (permissions, concurrent deletion) + // should not prevent session creation — callers get the same behavior as if the + // file did not exist. + return new HashMap<>(); + } + Map result = new HashMap<>(); + fileProps.forEach((k, v) -> result.put(k.toString(), v.toString())); + // Interpolate ${...} references against the loaded properties + fallback + for (Map.Entry entry : result.entrySet()) { + String value = entry.getValue(); + if (value != null && value.contains("${")) { + entry.setValue( + DefaultInterpolator.substVars(value, entry.getKey(), null, result, fallback::get, null, false)); + } + } + return result; + } - // session.getService(SettingsBuilder.class).convert() + /** + * Detects the Maven version by reading the pom.properties resource from the classpath. + * Falls back to reading from maven-impl's own pom.properties if maven-core is not available. + * If no version can be determined, returns {@code 0.0.0} as a sentinel value so that + * {@link Session#getMavenVersion()} is never null. + * + * @param lookup the lookup service + * @return the detected Maven version, never {@code null} + */ + @Nonnull + private static Version detectMavenVersion(Lookup lookup) { + String version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-core/pom.properties"); + if (version == null) { + version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-impl/pom.properties"); + } + if (version == null) { + version = "0.0.0"; + } + try { + return lookup.lookup(VersionParser.class).parseVersion(version); + } catch (Exception e) { + // Should not happen with "0.0.0", but be safe + return lookup.lookup(VersionParser.class).parseVersion("0.0.0"); + } + } - // settings.getDelegate().getRepositories().stream() - // .map(r -> SettingsUtilsV4.) - // defaultSession.getService(RepositoryFactory.class).createRemote() - // return defaultSession; + private static String loadVersionFromProperties(String resource) { + try (InputStream is = ApiRunner.class.getResourceAsStream("/" + resource)) { + if (is != null) { + Properties props = new Properties(); + props.load(is); + String version = props.getProperty("version", "").trim(); + if (!version.isEmpty() && !version.startsWith("${")) { + return version; + } + } + } catch (IOException e) { + // ignore + } + return null; } record DumbPackaging(String id, Type type, Map plugins) implements Packaging {} + + /** + * Provides the security dispatcher bindings needed for decrypting encrypted passwords + * in settings.xml. This class is only loaded when {@code plexus-sec-dispatcher} is confirmed + * to be on the classpath (see {@link #configureSecurityDispatchers}). + * + *

Supports both legacy ({@code {...}}) and Maven 4 master-key-based encrypted passwords.

+ */ + @SuppressWarnings("unused") + static class SecDispatcherBindings { + + @Provides + @Named(LegacyDispatcher.NAME) + static Dispatcher legacyDispatcher() { + return new LegacyDispatcher(); + } + + @Provides + @Named(MasterDispatcher.NAME) + static Dispatcher masterDispatcher(Map ciphers, Map sources) { + return new MasterDispatcher(ciphers, sources); + } + + @Provides + @Named(AESGCMNoPadding.CIPHER_ALG) + static Cipher aesCipher() { + return new AESGCMNoPadding(); + } + + @Provides + @Named(EnvMasterSource.NAME) + static MasterSource envSource() { + return new EnvMasterSource(); + } + + @Provides + @Named(GpgAgentMasterSource.NAME) + static MasterSource gpgAgentSource() { + return new GpgAgentMasterSource(); + } + + @Provides + @Named(PinEntryMasterSource.NAME) + static MasterSource pinEntrySource() { + return new PinEntryMasterSource(); + } + + @Provides + @Named(SystemPropertyMasterSource.NAME) + static MasterSource systemPropertySource() { + return new SystemPropertyMasterSource(); + } + } } diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java index 28aa6386ecf6..c4cc9282e45b 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java @@ -61,7 +61,7 @@ private static Model newModel(String groupId, String artifactId, String version) } @Test - void noRelocationReturnsNull() { + void noRelocationReturnsNull() throws Exception { Model model = Model.newBuilder().build(); Artifact result = source.relocatedTarget(null, newResult(), model); assertNull(result); diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java new file mode 100644 index 000000000000..4e4bf9f9aac8 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java @@ -0,0 +1,289 @@ +/* + * 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.standalone; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.maven.api.Session; +import org.apache.maven.api.settings.Server; +import org.apache.maven.api.settings.Settings; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link ApiRunner} settings handling and {@link ApiRunner.SecurityMode}. + */ +class ApiRunnerSettingsTest { + + @TempDir + Path tempDir; + + @Test + void testSecurityModeNone() { + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE); + assertNotNull(session); + assertNotNull(session.getSettings()); + } + + @Test + void testSecurityModeIfAvailable() { + // plexus-sec-dispatcher is on the test classpath, so dispatchers should be bound + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE); + assertNotNull(session); + assertNotNull(session.getSettings()); + } + + @Test + void testSecurityModeIfAvailableWarn() { + // Default mode — same as IF_AVAILABLE when sec-dispatcher is on classpath + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE_WARN); + assertNotNull(session); + assertNotNull(session.getSettings()); + } + + @Test + void testSecurityModeRequired() { + // plexus-sec-dispatcher is on the test classpath, so this should succeed + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.REQUIRED); + assertNotNull(session); + assertNotNull(session.getSettings()); + } + + @Test + void testDefaultSecurityMode() { + // Default createSession() uses IF_AVAILABLE_WARN + Session session = ApiRunner.createSession(); + assertNotNull(session); + assertNotNull(session.getSettings()); + } + + @Test + void testMavenVersionIsNeverNull() { + Session session = ApiRunner.createSession(); + assertNotNull(session.getMavenVersion(), "getMavenVersion() should never return null"); + } + + @Test + void testSettingsServersApplied() throws IOException { + Path m2Dir = tempDir.resolve(".m2"); + Files.createDirectories(m2Dir); + Files.writeString(m2Dir.resolve("settings.xml"), """ + + + + + my-repo + myuser + mypassword + + + + """); + + String oldHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", tempDir.toString()); + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE); + Settings settings = session.getSettings(); + assertNotNull(settings); + assertEquals(1, settings.getServers().size()); + Server server = settings.getServers().get(0); + assertEquals("my-repo", server.getId()); + assertEquals("myuser", server.getUsername()); + assertEquals("mypassword", server.getPassword()); + } finally { + System.setProperty("user.home", oldHome); + } + } + + @Test + void testSettingsMirrorsApplied() throws IOException { + Path m2Dir = tempDir.resolve(".m2"); + Files.createDirectories(m2Dir); + Files.writeString(m2Dir.resolve("settings.xml"), """ + + + + + my-mirror + https://mirror.example.com/maven2 + central + + + + """); + + String oldHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", tempDir.toString()); + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE); + Settings settings = session.getSettings(); + assertNotNull(settings); + assertEquals(1, settings.getMirrors().size()); + assertEquals("my-mirror", settings.getMirrors().get(0).getId()); + assertEquals( + "https://mirror.example.com/maven2", + settings.getMirrors().get(0).getUrl()); + assertEquals("central", settings.getMirrors().get(0).getMirrorOf()); + } finally { + System.setProperty("user.home", oldHome); + } + } + + @Test + void testSettingsRepositories() throws IOException { + Path m2Dir = tempDir.resolve(".m2"); + Files.createDirectories(m2Dir); + // Maven 4 top-level repositories in settings + Files.writeString(m2Dir.resolve("settings.xml"), """ + + + + + custom-repo + https://repo.example.com/maven2 + + + + """); + + String oldHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", tempDir.toString()); + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE); + Settings settings = session.getSettings(); + assertNotNull(settings); + assertTrue( + settings.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), + "Effective settings should include top-level repository"); + // Session should also expose them as remote repositories + assertTrue( + session.getRemoteRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())), + "Session should include repository from settings"); + } finally { + System.setProperty("user.home", oldHome); + } + } + + @Test + void testLegacyEncryptedPasswordDecrypted() throws IOException { + Path m2Dir = tempDir.resolve(".m2"); + Files.createDirectories(m2Dir); + // Legacy master password from the existing IT test resources (mng-8379) + // Master password "testtest" encrypted with the default settings.security key + Files.writeString(m2Dir.resolve("settings-security.xml"), """ + + + {1wQaa6S/o8MH7FnaTNL53XmhT5O0SEGXQi3gC49o6OY=} + + """); + // Server password "testtest" encrypted with the master password above + Files.writeString(m2Dir.resolve("settings.xml"), """ + + + + + testserver + testuser + {BteqUEnqHecHM7MZfnj9FwLcYbdInWxou1C929Txa0A=} + + + + """); + + String oldHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", tempDir.toString()); + // With dispatchers active, the legacy password should be decrypted + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE); + Settings settings = session.getSettings(); + assertNotNull(settings); + assertEquals(1, settings.getServers().size()); + Server server = settings.getServers().get(0); + assertEquals("testserver", server.getId()); + assertEquals("testuser", server.getUsername()); + assertEquals("testtest", server.getPassword()); + } finally { + System.setProperty("user.home", oldHome); + } + } + + @Test + void testPlaintextPasswordUnchangedWithSecurityModeNone() throws IOException { + // SecurityMode.NONE prevents ApiRunner from binding its own SecDispatcherBindings. + // Note: in a test environment, discover() may still find a test-scoped SecDispatcherProvider; + // this test verifies that plaintext passwords pass through correctly in all modes. + Path m2Dir = tempDir.resolve(".m2"); + Files.createDirectories(m2Dir); + Files.writeString(m2Dir.resolve("settings.xml"), """ + + + + + testserver + testuser + plaintext-password + + + + """); + + String oldHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", tempDir.toString()); + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE); + Settings settings = session.getSettings(); + assertNotNull(settings); + assertEquals(1, settings.getServers().size()); + Server server = settings.getServers().get(0); + assertEquals("plaintext-password", server.getPassword()); + } finally { + System.setProperty("user.home", oldHome); + } + } + + @Test + void testSettingsOfflineMode() throws IOException { + Path m2Dir = tempDir.resolve(".m2"); + Files.createDirectories(m2Dir); + Files.writeString(m2Dir.resolve("settings.xml"), """ + + + true + + """); + + String oldHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", tempDir.toString()); + Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE); + Settings settings = session.getSettings(); + assertNotNull(settings); + assertTrue(settings.isOffline(), "Settings should have offline mode enabled"); + } finally { + System.setProperty("user.home", oldHome); + } + } +}