messages = new ArrayList<>();
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
index 45ca17114bd3..a99cbc2780d8 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactory.java
@@ -105,15 +105,20 @@ public class DefaultRepositorySystemSessionFactory implements RepositorySystemSe
* User property selecting how server credentials configured in settings are scoped to repositories:
*
* - {@code origin} (default): credentials for a server id are only used with a repository whose
- * origin (protocol, host and port) matches a repository or mirror declared with the same id in
- * settings or on the command line. For server ids without any such declared repository (for
- * example pure deployment servers whose URL comes from the project's
+ * origin (protocol, host and port) matches an origin declared for the same id, either by a
+ * repository or mirror declared with that id in settings or on the command line, or by the
+ * {@code } of that server in settings. For server ids without any such declared
+ * origin (for example pure deployment servers whose URL comes from the project's
* {@code distributionManagement}), credentials are used as before, but a warning identifying the
* target origin is emitted.
* - {@code strict}: like {@code origin}, but credentials are refused for server ids that have no
- * repository or mirror declared in settings or on the command line.
+ * declared origin at all.
* - {@code id}: legacy behavior, credentials are matched by server id only.
*
+ *
+ * Repositories declared inside a settings {@code } only contribute an origin when the profile
+ * id is listed in {@code }; profiles activated through {@code } or
+ * {@code -P} contribute none, which is what {@code } is for.
*
* @since 4.0.0
*/
@@ -214,8 +219,9 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request)
.buildVersionFilter(mergedProps.get(Constants.MAVEN_VERSION_FILTER), this::parseVersionConstraint)
.ifPresent(sessionBuilder::setVersionFilter);
- // origins of the repositories and mirrors the operator declared for a given server id, used below
- // to scope that id's credentials to the origin(s) it was actually configured for
+ // origins of the repositories and mirrors the operator declared for a given server id, completed
+ // below with the origins declared on the servers themselves, used to scope that id's credentials
+ // to the origin(s) it was actually configured for
Map> declaredRepositoryOrigins = new HashMap<>();
DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
@@ -260,6 +266,13 @@ public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request)
authBuilder.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
authSelector.add(server.getId(), authBuilder.build());
+ // origins the operator bound to this id explicitly, added to the ones collected above from the
+ // mirrors and the repositories of the request
+ for (String repositoryOrigin : server.getRepositoryOrigins()) {
+ OriginBoundAuthenticationSelector.addOrigin(
+ declaredRepositoryOrigins, server.getId(), repositoryOrigin);
+ }
+
if (server.getConfiguration() != null) {
XmlNode dom = server.getDelegate().getConfiguration();
List children = dom.children().stream()
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/OriginBoundAuthenticationSelector.java b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/OriginBoundAuthenticationSelector.java
index 030b38742d32..a9f16621af34 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/aether/OriginBoundAuthenticationSelector.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/aether/OriginBoundAuthenticationSelector.java
@@ -38,10 +38,11 @@
* port) of the repository or mirror the operator declared for the same server id.
*
* A repository's id and its origin are independent: this selector serves a server id's credentials
- * only to a repository whose origin matches one the operator declared for that id, in settings or on
- * the command line. Ids with no operator-declared origin keep the previous behaviour unless
- * {@code strict} scope is requested, and a warning naming the target origin is emitted once per
- * id/origin pair.
+ * only to a repository whose origin matches one the operator declared for that id, either by declaring
+ * a repository or mirror with that id in settings or on the command line, or by listing the origin in
+ * that server's {@code } in settings. Ids with no operator-declared origin keep the
+ * previous behaviour unless {@code strict} scope is requested, and a warning naming the target origin
+ * is emitted once per id/origin pair.
*
* @see DefaultRepositorySystemSessionFactory#MAVEN_REPOSITORY_CREDENTIAL_SCOPE
*/
@@ -135,8 +136,10 @@ public Authentication getAuthentication(RemoteRepository repository) {
id,
origin,
"Not using credentials of server '" + id + "' for repository " + repository.getUrl()
- + ": the repository or mirror declared for this id resides at " + origins
- + ". Set "
+ + ": the origins declared for this id are " + origins
+ + ". Add " + originHint(origin)
+ + " to of that server in settings if these credentials belong"
+ + " there, or set "
+ DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE + "="
+ SCOPE_ID + " to restore legacy id-only credential matching.");
return null;
@@ -147,8 +150,10 @@ public Authentication getAuthentication(RemoteRepository repository) {
origin,
"Not using credentials of server '" + id + "' for repository " + repository.getUrl()
+ ": no repository or mirror with this id is declared in settings or on the command"
- + " line, and " + DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
- + "=" + SCOPE_STRICT + " is in effect.");
+ + " line and the server declares no , and "
+ + DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
+ + "=" + SCOPE_STRICT + " is in effect. Declare " + originHint(origin)
+ + " in of that server in settings to allow it.");
return null;
}
warnOnce(
@@ -156,12 +161,19 @@ public Authentication getAuthentication(RemoteRepository repository) {
origin,
"Using credentials of server '" + id + "' for repository " + repository.getUrl()
+ ", although no repository or mirror with this id is declared in settings or on the"
- + " command line. Set "
+ + " command line and the server declares no . Declare "
+ + originHint(origin)
+ + " in of that server in settings to bind these credentials"
+ + " explicitly, or set "
+ DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
+ "=" + SCOPE_STRICT + " to refuse such credential use.");
return auth;
}
+ private static String originHint(String origin) {
+ return origin != null ? "'" + origin + "'" : "its origin";
+ }
+
private void warnOnce(String id, String origin, String message) {
if (reported.add(id + "->" + origin)) {
logger.warn(message);
diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java
index c6d30a13e562..aae3f8c5a352 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/internal/aether/DefaultRepositorySystemSessionFactoryTest.java
@@ -40,12 +40,15 @@
import org.apache.maven.impl.InternalSession;
import org.apache.maven.internal.impl.DefaultTypeRegistry;
import org.apache.maven.rtinfo.RuntimeInformation;
+import org.apache.maven.settings.Mirror;
import org.apache.maven.settings.Server;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.testing.PlexusTest;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.collection.VersionFilterBuilder;
+import org.eclipse.aether.repository.AuthenticationSelector;
+import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.version.VersionScheme;
import org.junit.jupiter.api.Test;
@@ -437,6 +440,105 @@ void transportConfigurationTest() throws InvalidRepositoryException {
properties.remove("maven.resolver.transport");
}
+ @Test
+ void credentialsServedForOriginDeclaredOnlyByServer() throws InvalidRepositoryException {
+ MavenExecutionRequest request = requestWithServer(
+ serverWithRepositoryOrigins("internal", "https://repo.example.org", "https://mirror.example.org:8443"));
+
+ AuthenticationSelector selector = authenticationSelector(request);
+
+ assertNotNull(selector.getAuthentication(repository("internal", "https://repo.example.org/releases/")));
+ assertNotNull(selector.getAuthentication(repository("internal", "https://mirror.example.org:8443/repo/")));
+ assertNull(selector.getAuthentication(repository("internal", "https://evil.example.org/releases/")));
+ }
+
+ @Test
+ void credentialsServedForOriginDeclaredOnlyByServerInStrictScope() throws InvalidRepositoryException {
+ MavenExecutionRequest request =
+ requestWithServer(serverWithRepositoryOrigins("internal", "https://repo.example.org"));
+ Properties properties = new Properties();
+ properties.put(DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE, "strict");
+ request.setSystemProperties(properties);
+
+ AuthenticationSelector selector = authenticationSelector(request);
+
+ assertNotNull(selector.getAuthentication(repository("internal", "https://repo.example.org/releases/")));
+ assertNull(selector.getAuthentication(repository("internal", "https://evil.example.org/releases/")));
+ }
+
+ @Test
+ void serverWithoutRepositoryOriginsIsRefusedInStrictScope() throws InvalidRepositoryException {
+ MavenExecutionRequest request = requestWithServer(serverWithRepositoryOrigins("internal"));
+ Properties properties = new Properties();
+ properties.put(DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE, "strict");
+ request.setSystemProperties(properties);
+
+ AuthenticationSelector selector = authenticationSelector(request);
+
+ assertNull(selector.getAuthentication(repository("internal", "https://repo.example.org/releases/")));
+ }
+
+ @Test
+ void serverRepositoryOriginsAddToMirrorOrigins() throws InvalidRepositoryException {
+ MavenExecutionRequest request =
+ requestWithServer(serverWithRepositoryOrigins("internal", "https://repo.example.org"));
+ Mirror mirror = new Mirror();
+ mirror.setId("internal");
+ mirror.setUrl("https://mirror.example.org/repo/");
+ mirror.setMirrorOf("*");
+ request.setMirrors(new ArrayList<>(List.of(mirror)));
+
+ AuthenticationSelector selector = authenticationSelector(request);
+
+ assertNotNull(selector.getAuthentication(repository("internal", "https://mirror.example.org/repo/")));
+ assertNotNull(selector.getAuthentication(repository("internal", "https://repo.example.org/releases/")));
+ assertNull(selector.getAuthentication(repository("internal", "https://evil.example.org/releases/")));
+ }
+
+ @Test
+ void malformedServerRepositoryOriginIsIgnored() throws InvalidRepositoryException {
+ MavenExecutionRequest request = requestWithServer(serverWithRepositoryOrigins("internal", "not an origin"));
+ Properties properties = new Properties();
+ properties.put(DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE, "strict");
+ request.setSystemProperties(properties);
+
+ AuthenticationSelector selector = authenticationSelector(request);
+
+ assertNull(selector.getAuthentication(repository("internal", "https://repo.example.org/releases/")));
+ }
+
+ private static Server serverWithRepositoryOrigins(String id, String... repositoryOrigins) {
+ Server server = new Server();
+ server.setId(id);
+ server.setUsername("jason");
+ server.setPassword("abc123");
+ server.setRepositoryOrigins(List.of(repositoryOrigins));
+ return server;
+ }
+
+ private MavenExecutionRequest requestWithServer(Server server) throws InvalidRepositoryException {
+ MavenExecutionRequest request = new DefaultMavenExecutionRequest();
+ request.setLocalRepository(getLocalRepository());
+ request.setServers(new ArrayList<>(List.of(server)));
+ return request;
+ }
+
+ private AuthenticationSelector authenticationSelector(MavenExecutionRequest request) {
+ DefaultRepositorySystemSessionFactory systemSessionFactory = new DefaultRepositorySystemSessionFactory(
+ aetherRepositorySystem,
+ eventSpyDispatcher,
+ information,
+ defaultTypeRegistry,
+ versionScheme,
+ Collections.emptyMap(),
+ versionFilterBuilder);
+ return systemSessionFactory.newRepositorySession(request).getAuthenticationSelector();
+ }
+
+ private static RemoteRepository repository(String id, String url) {
+ return new RemoteRepository.Builder(id, "default", url).build();
+ }
+
protected ArtifactRepository getLocalRepository() throws InvalidRepositoryException {
File repoDir = new File(getBasedir(), "target/local-repo").getAbsoluteFile();
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java
index e061346900c5..39f8d47bfc94 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java
@@ -228,6 +228,7 @@ private Settings readSettings(
.filePermissions(null)
.directoryPermissions(null)
.aliases(List.of())
+ .repositoryOrigins(List.of())
.build())
.toList())
.build();
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsValidator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsValidator.java
index 4887dffe725f..f79e15c19817 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsValidator.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsValidator.java
@@ -18,6 +18,8 @@
*/
package org.apache.maven.impl;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -76,6 +78,10 @@ public void validate(Settings settings, boolean isProjectSettings, ProblemCollec
if (!server.getAliases().isEmpty()) {
addViolation(problems, BuilderProblem.Severity.WARNING, serverField + ".aliases", null, msgP);
}
+ if (!server.getRepositoryOrigins().isEmpty()) {
+ addViolation(
+ problems, BuilderProblem.Severity.WARNING, serverField + ".repositoryOrigins", null, msgP);
+ }
}
}
@@ -142,6 +148,8 @@ public void validate(Settings settings, boolean isProjectSettings, ProblemCollec
"must be unique across all server ids and aliases but found duplicate alias " + alias);
}
}
+
+ validateRepositoryOrigins(problems, server, i);
}
}
@@ -289,6 +297,87 @@ private void validateRepositories(
* string.length == 0
*
*/
+ private static void validateRepositoryOrigins(ProblemCollector problems, Server server, int index) {
+ for (int o = 0; o < server.getRepositoryOrigins().size(); o++) {
+ String repositoryOrigin = server.getRepositoryOrigins().get(o);
+ String fieldName = "servers.server[" + index + "].repositoryOrigins[" + o + "]";
+
+ if (!validateStringNotEmpty(problems, fieldName, repositoryOrigin, server.getId())) {
+ continue;
+ }
+
+ // settings are interpolated before they are validated, so a placeholder left here is a defect
+ // and would silently never match any repository
+ if (repositoryOrigin.contains("${")) {
+ addViolation(
+ problems,
+ BuilderProblem.Severity.ERROR,
+ fieldName,
+ server.getId(),
+ "contains an unresolved property placeholder: '" + repositoryOrigin + "'");
+ continue;
+ }
+
+ String invalid = invalidRepositoryOriginReason(repositoryOrigin);
+ if (invalid != null) {
+ addViolation(problems, BuilderProblem.Severity.ERROR, fieldName, server.getId(), invalid);
+ continue;
+ }
+
+ String ignored = ignoredRepositoryOriginPartsReason(repositoryOrigin);
+ if (ignored != null) {
+ addViolation(problems, BuilderProblem.Severity.WARNING, fieldName, server.getId(), ignored);
+ }
+ }
+ }
+
+ /**
+ * Parses a {@code } value the way the credential scoping does; see
+ * {@code OriginBoundAuthenticationSelector#originOf(String)} in maven-core, which is the authority
+ * on what an origin is. Returns {@code null} when the value is not a URI at all.
+ */
+ private static URI parseRepositoryOrigin(String value) {
+ try {
+ return new URI(value).parseServerAuthority();
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ /**
+ * @return the reason why no origin can be derived from the given value, or {@code null} if one can
+ */
+ private static String invalidRepositoryOriginReason(String value) {
+ URI uri = parseRepositoryOrigin(value);
+ if (uri == null) {
+ return "must be a repository origin of the form scheme://host[:port] but found '" + value + "'";
+ }
+ if (uri.getScheme() == null) {
+ return "must start with a scheme, for example https://repo.example.org, but found '" + value + "'";
+ }
+ if (uri.getHost() == null) {
+ return "must name a host, for example https://repo.example.org, but found '" + value + "'";
+ }
+ if (uri.getUserInfo() != null) {
+ return "must not carry user information but found '" + value + "'";
+ }
+ return null;
+ }
+
+ /**
+ * @return the reason why parts of the given value are ignored, or {@code null} if it is a bare origin
+ */
+ private static String ignoredRepositoryOriginPartsReason(String value) {
+ URI uri = parseRepositoryOrigin(value);
+ String path = uri.getRawPath();
+ boolean extraPath = path != null && !path.isEmpty() && !"/".equals(path);
+ if (!extraPath && uri.getRawQuery() == null && uri.getRawFragment() == null) {
+ return null;
+ }
+ return "is a repository origin, not a repository URL; only '" + uri.getScheme() + "://" + uri.getAuthority()
+ + "' of '" + value + "' is used";
+ }
+
private static boolean validateStringEmpty(
ProblemCollector problems, String fieldName, String string, String message) {
if (string == null || string.isEmpty()) {
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java
index f4a9fef36553..16210f65f46d 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java
@@ -161,6 +161,23 @@ void testSettingsWithDuplicateServersIds() throws Exception {
problems.problems().findFirst().orElseThrow().getMessage());
}
+ @Test
+ void testSettingsWithServerRepositoryOrigins() {
+ Settings settings = execute("settings-servers-4").getEffectiveSettings();
+
+ List servers = settings.getServers();
+ assertEquals(2, servers.size());
+
+ List repositoryOrigins = List.of("https://repo.example.org", "https://mirror.example.org:8443");
+
+ Server server1 = getServerById(servers, "server-1");
+ assertEquals(repositoryOrigins, server1.getRepositoryOrigins());
+
+ // an alias is the same credentials under another id, so it is bound to the same origins
+ Server server11 = getServerById(servers, "server-11");
+ assertEquals(repositoryOrigins, server11.getRepositoryOrigins());
+ }
+
@Test
void testRelativeLocalRepositoryIsResolvedToAbsolute() {
Settings settings = execute("settings-relative-local-repo").getEffectiveSettings();
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsParserTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsParserTest.java
index 0fd89f4c4935..ecfe6a21f947 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsParserTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsParserTest.java
@@ -410,6 +410,34 @@ void projectSettingsRestrictionsApplyToCustomParser() throws Exception {
assertTrue(result.getProblems().hasWarningProblems());
}
+ @Test
+ void projectSettingsCannotWidenServerCredentialOrigins() throws Exception {
+ var result = builder(Map.of())
+ .build(SettingsBuilderRequest.builder()
+ .session(mock(Session.class))
+ .userSettingsSource(source(
+ "user.xml",
+ "repository"
+ + "user"
+ + "https://good.example.org"
+ + ""))
+ .projectSettingsSource(source(
+ "project.xml",
+ "repository"
+ + "https://evil.example.org"
+ + ""))
+ .build());
+
+ // a server of the project settings is kept as a separate entry of the same id, so the origins of
+ // every entry matter: none of them may come from the project
+ List repositoryOrigins = result.getEffectiveSettings().getServers().stream()
+ .filter(server -> "repository".equals(server.getId()))
+ .flatMap(server -> server.getRepositoryOrigins().stream())
+ .toList();
+ assertEquals(List.of("https://good.example.org"), repositoryOrigins);
+ assertTrue(result.getProblems().hasWarningProblems());
+ }
+
@Test
void customSettingsDecryptionFailureDoesNotExposeCredentials() throws Exception {
String encrypted = "{L6L/HbmrY+cH+sNkphn-corrupted-q3fguYepTpM04WlIXb8nB1pk=}";
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsValidatorTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsValidatorTest.java
index 46ae074de0eb..aaee1eecad3e 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsValidatorTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsValidatorTest.java
@@ -117,4 +117,110 @@ void testValidateServerIdAliasesWithEmptyValue() {
"'servers.server[0].aliases[0]' for server-1 is missing",
problems.problems().findFirst().orElseThrow().getMessage());
}
+
+ @Test
+ void testValidateServerRepositoryOrigins() {
+ Server server = Server.newBuilder()
+ .id("server-1")
+ .repositoryOrigins(List.of(
+ "https://repo.example.org",
+ "https://mirror.example.org:8443",
+ "HTTP://Repo.Example.Org:80",
+ "https://repo.example.org/"))
+ .build();
+
+ Settings settings = Settings.newBuilder().servers(List.of(server)).build();
+
+ ProblemCollector problems = validator.validate(settings);
+ assertEquals(0, problems.totalProblemsReported());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginWithoutScheme() {
+ ProblemCollector problems = validateRepositoryOrigin("repo.example.org");
+ assertEquals(1, problems.totalProblemsReported());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins[0]' for server-1 must start with a scheme,"
+ + " for example https://repo.example.org, but found 'repo.example.org'",
+ problems.problems().findFirst().orElseThrow().getMessage());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginWithoutHost() {
+ ProblemCollector problems = validateRepositoryOrigin("file:/tmp/repo");
+ assertEquals(1, problems.totalProblemsReported());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins[0]' for server-1 must name a host,"
+ + " for example https://repo.example.org, but found 'file:/tmp/repo'",
+ problems.problems().findFirst().orElseThrow().getMessage());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginWithUserInfo() {
+ ProblemCollector problems = validateRepositoryOrigin("https://user:pwd@repo.example.org");
+ assertEquals(1, problems.totalProblemsReported());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins[0]' for server-1 must not carry user information"
+ + " but found 'https://user:pwd@repo.example.org'",
+ problems.problems().findFirst().orElseThrow().getMessage());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginWithPlaceholder() {
+ ProblemCollector problems = validateRepositoryOrigin("${env.REPO_URL}");
+ assertEquals(1, problems.totalProblemsReported());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins[0]' for server-1 contains an unresolved property"
+ + " placeholder: '${env.REPO_URL}'",
+ problems.problems().findFirst().orElseThrow().getMessage());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginEmpty() {
+ ProblemCollector problems = validateRepositoryOrigin("");
+ assertEquals(1, problems.totalProblemsReported());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins[0]' for server-1 is missing",
+ problems.problems().findFirst().orElseThrow().getMessage());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginWithPathIsOnlyWarned() {
+ ProblemCollector problems = validateRepositoryOrigin("https://repo.example.org/releases/");
+ assertEquals(1, problems.totalProblemsReported());
+ BuilderProblem problem = problems.problems().findFirst().orElseThrow();
+ assertEquals(BuilderProblem.Severity.WARNING, problem.getSeverity());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins[0]' for server-1 is a repository origin,"
+ + " not a repository URL; only 'https://repo.example.org' of"
+ + " 'https://repo.example.org/releases/' is used",
+ problem.getMessage());
+ }
+
+ @Test
+ void testValidateServerRepositoryOriginsOnProjectSettings() {
+ Server server = Server.newBuilder()
+ .id("server-1")
+ .repositoryOrigins(List.of("https://repo.example.org"))
+ .build();
+
+ Settings settings = Settings.newBuilder().servers(List.of(server)).build();
+
+ ProblemCollector problems = validator.validate(settings, true);
+ assertEquals(1, problems.totalProblemsReported());
+ assertEquals(
+ "'servers.server[0].repositoryOrigins' are not supported on project settings.",
+ problems.problems().findFirst().orElseThrow().getMessage());
+ }
+
+ private ProblemCollector validateRepositoryOrigin(String repositoryOrigin) {
+ Server server = Server.newBuilder()
+ .id("server-1")
+ .repositoryOrigins(List.of(repositoryOrigin))
+ .build();
+
+ Settings settings = Settings.newBuilder().servers(List.of(server)).build();
+
+ return validator.validate(settings);
+ }
}
diff --git a/impl/maven-impl/src/test/resources/settings/settings-servers-4.xml b/impl/maven-impl/src/test/resources/settings/settings-servers-4.xml
new file mode 100644
index 000000000000..4c3923b1ef93
--- /dev/null
+++ b/impl/maven-impl/src/test/resources/settings/settings-servers-4.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+ server-1
+ username1
+ password1
+
+ server-11
+
+
+ https://repo.example.org
+ https://mirror.example.org:8443
+
+
+
+
+