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.aether;
20  
21  import java.net.URI;
22  import java.net.URISyntaxException;
23  import java.util.HashSet;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.concurrent.ConcurrentHashMap;
28  
29  import org.codehaus.plexus.logging.Logger;
30  import org.eclipse.aether.repository.Authentication;
31  import org.eclipse.aether.repository.AuthenticationSelector;
32  import org.eclipse.aether.repository.RemoteRepository;
33  
34  import static java.util.Objects.requireNonNull;
35  
36  /**
37   * An {@link AuthenticationSelector} that scopes server credentials to the origin (protocol, host and
38   * port) of the repository or mirror the operator declared for the same server id.
39   * <p>
40   * A repository's id and its origin are independent: this selector serves a server id's credentials
41   * only to a repository whose origin matches one the operator declared for that id, either by declaring
42   * a repository or mirror with that id in settings or on the command line, or by listing the origin in
43   * that server's {@code <repositoryOrigins>} in settings. Ids with no operator-declared origin keep the
44   * previous behaviour unless {@code strict} scope is requested, and a warning naming the target origin
45   * is emitted once per id/origin pair.
46   *
47   * @see DefaultRepositorySystemSessionFactory#MAVEN_REPOSITORY_CREDENTIAL_SCOPE
48   */
49  class OriginBoundAuthenticationSelector implements AuthenticationSelector {
50      /**
51       * Credentials are bound to operator-declared origins; ids without a declared origin keep legacy
52       * behavior, with a warning.
53       */
54      static final String SCOPE_ORIGIN = "origin";
55  
56      /**
57       * Credentials are bound to operator-declared origins; ids without a declared origin get no
58       * credentials.
59       */
60      static final String SCOPE_STRICT = "strict";
61  
62      /**
63       * Legacy behavior: credentials are matched by server id only.
64       */
65      static final String SCOPE_ID = "id";
66  
67      private final AuthenticationSelector delegate;
68  
69      private final Map<String, Set<String>> declaredOrigins;
70  
71      private final boolean strict;
72  
73      private final Logger logger;
74  
75      private final Set<String> reported = ConcurrentHashMap.newKeySet();
76  
77      private OriginBoundAuthenticationSelector(
78              AuthenticationSelector delegate, Map<String, Set<String>> declaredOrigins, boolean strict, Logger logger) {
79          this.delegate = requireNonNull(delegate, "delegate");
80          this.declaredOrigins = requireNonNull(declaredOrigins, "declaredOrigins");
81          this.strict = strict;
82          this.logger = requireNonNull(logger, "logger");
83      }
84  
85      /**
86       * Wraps the given selector according to the requested credential scope.
87       *
88       * @param delegate the selector holding the actual credentials, keyed by server id
89       * @param credentialScope one of {@link #SCOPE_ORIGIN}, {@link #SCOPE_STRICT} or {@link #SCOPE_ID}
90       * @param declaredOrigins origins of operator-declared repositories and mirrors, keyed by id
91       * @param logger logger used to report id/origin mismatches
92       * @return the delegate itself for {@link #SCOPE_ID}, an origin-bound wrapper otherwise
93       */
94      static AuthenticationSelector wrap(
95              AuthenticationSelector delegate,
96              String credentialScope,
97              Map<String, Set<String>> declaredOrigins,
98              Logger logger) {
99          if (SCOPE_ID.equals(credentialScope)) {
100             return delegate;
101         } else if (SCOPE_ORIGIN.equals(credentialScope) || SCOPE_STRICT.equals(credentialScope)) {
102             return new OriginBoundAuthenticationSelector(
103                     delegate, declaredOrigins, SCOPE_STRICT.equals(credentialScope), logger);
104         } else {
105             throw new IllegalArgumentException("Unknown value '" + credentialScope + "' for "
106                     + DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
107                     + ". Supported values are: " + SCOPE_ORIGIN + ", " + SCOPE_STRICT + ", " + SCOPE_ID);
108         }
109     }
110 
111     /**
112      * Records the origin of an operator-declared repository or mirror for the given id. URLs without a
113      * parseable server authority (for example {@code file:} URLs) are ignored.
114      */
115     static void addOrigin(Map<String, Set<String>> declaredOrigins, String id, String url) {
116         String origin = originOf(url);
117         if (id != null && origin != null) {
118             declaredOrigins.computeIfAbsent(id, k -> new HashSet<>()).add(origin);
119         }
120     }
121 
122     @Override
123     public Authentication getAuthentication(RemoteRepository repository) {
124         Authentication auth = delegate.getAuthentication(repository);
125         if (auth == null) {
126             return null;
127         }
128         String id = repository.getId();
129         String origin = originOf(repository.getUrl());
130         Set<String> origins = declaredOrigins.get(id);
131         if (origins != null && !origins.isEmpty()) {
132             if (origin != null && origins.contains(origin)) {
133                 return auth;
134             }
135             warnOnce(
136                     id,
137                     origin,
138                     "Not using credentials of server '" + id + "' for repository " + repository.getUrl()
139                             + ": the origins declared for this id are " + origins
140                             + ". Add " + originHint(origin)
141                             + " to <repositoryOrigins> of that server in settings if these credentials belong"
142                             + " there, or set "
143                             + DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE + "="
144                             + SCOPE_ID + " to restore legacy id-only credential matching.");
145             return null;
146         }
147         if (strict) {
148             warnOnce(
149                     id,
150                     origin,
151                     "Not using credentials of server '" + id + "' for repository " + repository.getUrl()
152                             + ": no repository or mirror with this id is declared in settings or on the command"
153                             + " line and the server declares no <repositoryOrigins>, and "
154                             + DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
155                             + "=" + SCOPE_STRICT + " is in effect. Declare " + originHint(origin)
156                             + " in <repositoryOrigins> of that server in settings to allow it.");
157             return null;
158         }
159         warnOnce(
160                 id,
161                 origin,
162                 "Using credentials of server '" + id + "' for repository " + repository.getUrl()
163                         + ", although no repository or mirror with this id is declared in settings or on the"
164                         + " command line and the server declares no <repositoryOrigins>. Declare "
165                         + originHint(origin)
166                         + " in <repositoryOrigins> of that server in settings to bind these credentials"
167                         + " explicitly, or set "
168                         + DefaultRepositorySystemSessionFactory.MAVEN_REPOSITORY_CREDENTIAL_SCOPE
169                         + "=" + SCOPE_STRICT + " to refuse such credential use.");
170         return auth;
171     }
172 
173     private static String originHint(String origin) {
174         return origin != null ? "'" + origin + "'" : "its origin";
175     }
176 
177     private void warnOnce(String id, String origin, String message) {
178         if (reported.add(id + "->" + origin)) {
179             logger.warn(message);
180         }
181     }
182 
183     /**
184      * Returns the normalized origin ({@code protocol://host[:port]}, lower-cased, default http/https
185      * ports elided) of the given URL, or {@code null} if the URL has no parseable server authority.
186      */
187     static String originOf(String url) {
188         if (url == null) {
189             return null;
190         }
191         try {
192             URI uri = new URI(url).parseServerAuthority();
193             String scheme = uri.getScheme();
194             String host = uri.getHost();
195             if (scheme == null || host == null) {
196                 return null;
197             }
198             scheme = scheme.toLowerCase(Locale.ROOT);
199             host = host.toLowerCase(Locale.ROOT);
200             int port = uri.getPort();
201             if ((port == 80 && "http".equals(scheme)) || (port == 443 && "https".equals(scheme))) {
202                 port = -1;
203             }
204             return port >= 0 ? scheme + "://" + host + ":" + port : scheme + "://" + host;
205         } catch (URISyntaxException e) {
206             return null;
207         }
208     }
209 }