OriginBoundAuthenticationSelector.java
/*
* 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.internal.aether;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationSelector;
import org.eclipse.aether.repository.RemoteRepository;
import org.slf4j.Logger;
import static java.util.Objects.requireNonNull;
/**
* An {@link AuthenticationSelector} that scopes server credentials to the origin (protocol, host and
* 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.
*
* @see DefaultRepositorySystemSessionFactory#MAVEN_REPOSITORY_CREDENTIAL_SCOPE
*/
class OriginBoundAuthenticationSelector implements AuthenticationSelector {
/**
* Credentials are bound to operator-declared origins; ids without a declared origin keep legacy
* behavior, with a warning.
*/
static final String SCOPE_ORIGIN = "origin";
/**
* Credentials are bound to operator-declared origins; ids without a declared origin get no
* credentials.
*/
static final String SCOPE_STRICT = "strict";
/**
* Legacy behavior: credentials are matched by server id only.
*/
static final String SCOPE_ID = "id";
private final AuthenticationSelector delegate;
private final Map<String, Set<String>> declaredOrigins;
private final boolean strict;
private final Logger logger;
private final Set<String> reported = ConcurrentHashMap.newKeySet();
private OriginBoundAuthenticationSelector(
AuthenticationSelector delegate, Map<String, Set<String>> declaredOrigins, boolean strict, Logger logger) {
this.delegate = requireNonNull(delegate, "delegate");
this.declaredOrigins = requireNonNull(declaredOrigins, "declaredOrigins");
this.strict = strict;
this.logger = requireNonNull(logger, "logger");
}
/**
* Wraps the given selector according to the requested credential scope.
*
* @param delegate the selector holding the actual credentials, keyed by server id
* @param credentialScope one of {@link #SCOPE_ORIGIN}, {@link #SCOPE_STRICT} or {@link #SCOPE_ID}
* @param declaredOrigins origins of operator-declared repositories and mirrors, keyed by id
* @param logger logger used to report id/origin mismatches
* @return the delegate itself for {@link #SCOPE_ID}, an origin-bound wrapper otherwise
*/
static AuthenticationSelector wrap(
AuthenticationSelector delegate,
String credentialScope,
Map<String, Set<String>> declaredOrigins,
Logger logger) {
if (SCOPE_ID.equals(credentialScope)) {
return delegate;
} else if (SCOPE_ORIGIN.equals(credentialScope) || SCOPE_STRICT.equals(credentialScope)) {
return new OriginBoundAuthenticationSelector(
delegate, declaredOrigins, SCOPE_STRICT.equals(credentialScope), logger);
} else {
throw new IllegalArgumentException("Unknown value '" + credentialScope + "' for "
+ DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
+ ". Supported values are: " + SCOPE_ORIGIN + ", " + SCOPE_STRICT + ", " + SCOPE_ID);
}
}
/**
* Records the origin of an operator-declared repository or mirror for the given id. URLs without a
* parseable server authority (for example {@code file:} URLs) are ignored.
*/
static void addOrigin(Map<String, Set<String>> declaredOrigins, String id, String url) {
String origin = originOf(url);
if (id != null && origin != null) {
declaredOrigins.computeIfAbsent(id, k -> new HashSet<>()).add(origin);
}
}
@Override
public Authentication getAuthentication(RemoteRepository repository) {
Authentication auth = delegate.getAuthentication(repository);
if (auth == null) {
return null;
}
String id = repository.getId();
String origin = originOf(repository.getUrl());
Set<String> origins = declaredOrigins.get(id);
if (origins != null && !origins.isEmpty()) {
if (origin != null && origins.contains(origin)) {
return auth;
}
warnOnce(
id,
origin,
"Not using credentials of server '" + id + "' for repository " + repository.getUrl()
+ ": the repository or mirror declared for this id resides at " + origins
+ ". Set "
+ DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE + "="
+ SCOPE_ID + " to restore legacy id-only credential matching.");
return null;
}
if (strict) {
warnOnce(
id,
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.");
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 "
+ DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
+ "=" + SCOPE_STRICT + " to refuse such credential use.");
return auth;
}
private void warnOnce(String id, String origin, String message) {
if (reported.add(id + "->" + origin)) {
logger.warn(message);
}
}
/**
* Returns the normalized origin ({@code protocol://host[:port]}, lower-cased, default http/https
* ports elided) of the given URL, or {@code null} if the URL has no parseable server authority.
*/
static String originOf(String url) {
if (url == null) {
return null;
}
try {
URI uri = new URI(url).parseServerAuthority();
String scheme = uri.getScheme();
String host = uri.getHost();
if (scheme == null || host == null) {
return null;
}
scheme = scheme.toLowerCase(Locale.ROOT);
host = host.toLowerCase(Locale.ROOT);
int port = uri.getPort();
if ((port == 80 && "http".equals(scheme)) || (port == 443 && "https".equals(scheme))) {
port = -1;
}
return port >= 0 ? scheme + "://" + host + ":" + port : scheme + "://" + host;
} catch (URISyntaxException e) {
return null;
}
}
}