Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions api/maven-api-settings/src/main/mdo/settings.mdo
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,25 @@
<multiplicity>*</multiplicity>
</association>
</field>
<field>
<name>repositoryOrigins</name>
<version>1.3.0+</version>
<description>List of repository origins these credentials may be used with, each written as
{@code scheme://host} or {@code scheme://host:port}, for example {@code https://repo.example.org}
or {@code https://mirror.example.org:8443} - an origin, not a full repository URL.
Maven binds the credentials of a server to the origins of the repositories and mirrors declared
with the same id, see the {@code maven.repository.credentialScope} user property. The origins
listed here are added to those; nothing is replaced. Declaring them is needed when the repository
using these credentials is not known when the session is created, for example a repository
declared in a {@code settings.xml} profile that is not listed in {@code &lt;activeProfiles&gt;}
but activated through {@code &lt;activation&gt;} or {@code -P}.
Comparison is case insensitive and the default ports 80 (http) and 443 (https) are equivalent to
no port. This element is not supported in project settings and is dropped there.</description>
<association>
<type>String</type>
<multiplicity>*</multiplicity>
</association>
</field>
</fields>
</class>
<class>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import javax.inject.Named;
import javax.inject.Singleton;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -109,6 +111,8 @@ public void validate(Settings settings, SettingsProblemCollector problems) {
"must be unique across all server ids and aliases but found duplicate alias " + alias);
}
}

validateRepositoryOrigins(problems, server, i);
}
}

Expand Down Expand Up @@ -239,6 +243,81 @@ private void validateRepositories(SettingsProblemCollector problems, List<Reposi
* <li><code>string.length > 0</code>
* </ul>
*/
private static void validateRepositoryOrigins(SettingsProblemCollector 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 validated before they are interpolated here, so a placeholder is not something
// this validator can judge
if (repositoryOrigin.contains("${")) {
continue;
}

String invalid = invalidRepositoryOriginReason(repositoryOrigin);
if (invalid != null) {
addViolation(problems, Severity.ERROR, fieldName, server.getId(), invalid);
continue;
}

String ignored = ignoredRepositoryOriginPartsReason(repositoryOrigin);
if (ignored != null) {
addViolation(problems, Severity.WARNING, fieldName, server.getId(), ignored);
}
}
}

/**
* Parses a {@code <server><repositoryOrigins>} 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 validateStringNotEmpty(
SettingsProblemCollector problems, String fieldName, String string, String sourceHint) {
if (!validateNotNull(problems, fieldName, string, sourceHint)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,75 @@ void testValidateServerIdAliasesWithEmptyValue() {
assertContains(problems.messages.get(0), "'servers.server[0].aliases[0]' for server-1 is missing");
}

@Test
void testValidateServerRepositoryOrigins() {
SimpleProblemCollector problems = validateRepositoryOrigins(
"https://repo.example.org", "https://mirror.example.org:8443", "HTTP://Repo.Example.Org:80");
assertEquals(0, problems.messages.size());
}

@Test
void testValidateServerRepositoryOriginWithoutScheme() {
SimpleProblemCollector problems = validateRepositoryOrigins("repo.example.org");
assertEquals(1, problems.messages.size());
assertContains(
problems.messages.get(0),
"'servers.server[0].repositoryOrigins[0]' for server-1 must start with a scheme,"
+ " for example https://repo.example.org, but found 'repo.example.org'");
}

@Test
void testValidateServerRepositoryOriginWithoutHost() {
SimpleProblemCollector problems = validateRepositoryOrigins("file:/tmp/repo");
assertEquals(1, problems.messages.size());
assertContains(problems.messages.get(0), "must name a host");
}

@Test
void testValidateServerRepositoryOriginWithUserInfo() {
SimpleProblemCollector problems = validateRepositoryOrigins("https://user:pwd@repo.example.org");
assertEquals(1, problems.messages.size());
assertContains(problems.messages.get(0), "must not carry user information");
}

@Test
void testValidateServerRepositoryOriginEmpty() {
SimpleProblemCollector problems = validateRepositoryOrigins("");
assertEquals(1, problems.messages.size());
assertContains(problems.messages.get(0), "'servers.server[0].repositoryOrigins[0]' for server-1 is missing");
}

@Test
void testValidateServerRepositoryOriginWithPath() {
SimpleProblemCollector problems = validateRepositoryOrigins("https://repo.example.org/releases/");
assertEquals(1, problems.messages.size());
assertContains(
problems.messages.get(0),
"is a repository origin, not a repository URL; only 'https://repo.example.org'"
+ " of 'https://repo.example.org/releases/' is used");
}

@Test
void testValidateServerRepositoryOriginWithPlaceholderIsNotValidated() {
// this validator runs before the settings are interpolated
SimpleProblemCollector problems = validateRepositoryOrigins("${env.REPO_URL}");
assertEquals(0, problems.messages.size());
}

private SimpleProblemCollector validateRepositoryOrigins(String... repositoryOrigins) {
Settings settings = new Settings();
Server server = new Server();
server.setId("server-1");
for (String repositoryOrigin : repositoryOrigins) {
server.addRepositoryOrigin(repositoryOrigin);
}
settings.addServer(server);

SimpleProblemCollector problems = new SimpleProblemCollector();
validator.validate(settings, problems);
return problems;
}

private static class SimpleProblemCollector implements SettingsProblemCollector {

List<String> messages = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,20 @@ public class DefaultRepositorySystemSessionFactory implements RepositorySystemSe
* User property selecting how server credentials configured in settings are scoped to repositories:
* <ul>
* <li>{@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 <repositoryOrigins>} 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.</li>
* <li>{@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.</li>
* declared origin at all.</li>
* <li>{@code id}: legacy behavior, credentials are matched by server id only.</li>
* </ul>
* <p>
* Repositories declared inside a settings {@code <profile>} only contribute an origin when the profile
* id is listed in {@code <activeProfiles>}; profiles activated through {@code <activation>} or
* {@code -P} contribute none, which is what {@code <server><repositoryOrigins>} is for.
*
* @since 4.0.0
*/
Expand Down Expand Up @@ -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<String, Set<String>> declaredRepositoryOrigins = new HashMap<>();

DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
Expand Down Expand Up @@ -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<XmlNode> children = dom.children().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@
* port) of the repository or mirror the operator declared for the same server id.
* <p>
* 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 <repositoryOrigins>} 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
*/
Expand Down Expand Up @@ -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 <repositoryOrigins> 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;
Expand All @@ -147,21 +150,30 @@ 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 <repositoryOrigins>, and "
+ DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
+ "=" + SCOPE_STRICT + " is in effect. Declare " + originHint(origin)
+ " in <repositoryOrigins> of that server in settings to allow it.");
return null;
}
warnOnce(
id,
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 <repositoryOrigins>. Declare "
+ originHint(origin)
+ " in <repositoryOrigins> 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);
Expand Down
Loading
Loading