View Javadoc
1   /*
2    * Copyright (c) 2008 Sonatype, Inc. All rights reserved.
3    *
4    * This program is licensed to you under the Apache License Version 2.0,
5    * and you may not use this file except in compliance with the Apache License Version 2.0.
6    * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7    *
8    * Unless required by applicable law or agreed to in writing,
9    * software distributed under the Apache License Version 2.0 is distributed on an
10   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12   */
13  
14  package org.codehaus.plexus.components.secdispatcher.internal;
15  
16  import java.io.IOException;
17  import java.nio.file.Files;
18  import java.nio.file.Path;
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.StringTokenizer;
26  import java.util.stream.Collectors;
27  
28  import org.codehaus.plexus.components.secdispatcher.Dispatcher;
29  import org.codehaus.plexus.components.secdispatcher.DispatcherMeta;
30  import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
31  import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
32  import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
33  import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
34  
35  import static java.util.Objects.requireNonNull;
36  
37  /**
38   * Note: this implementation is NOT a JSR330 component. Integrating apps anyway want to customize it (at least
39   * the name and location of configuration file), so instead as before (providing "bad" configuration file just
40   * to have one), it is the duty of integrator to wrap and "finish" the implementation in a way it suits the
41   * integrator. Also, using "globals" like Java System Properties are bad thing, and it is integrator who knows
42   * what is needed anyway.
43   * <p>
44   * Recommended way for integration is to create JSR330 {@link javax.inject.Provider}.
45   *
46   * @author Oleg Gusakov
47   */
48  public class DefaultSecDispatcher implements SecDispatcher {
49      public static final String SHIELD_BEGIN = "{";
50      public static final String SHIELD_END = "}";
51      public static final String ATTR_START = "[";
52      public static final String ATTR_STOP = "]";
53  
54      protected final Map<String, Dispatcher> dispatchers;
55      protected final Path configurationFile;
56  
57      public DefaultSecDispatcher(Map<String, Dispatcher> dispatchers, Path configurationFile) {
58          this.dispatchers = requireNonNull(dispatchers);
59          this.configurationFile = requireNonNull(configurationFile);
60  
61          // file may or may not exist, but one thing is certain: it cannot be an exiting directory
62          if (Files.isDirectory(configurationFile)) {
63              throw new IllegalArgumentException("configurationFile cannot be a directory");
64          }
65      }
66  
67      @Override
68      public Set<DispatcherMeta> availableDispatchers() {
69          return Set.copyOf(
70                  dispatchers.entrySet().stream().map(this::dispatcherMeta).collect(Collectors.toSet()));
71      }
72  
73      private DispatcherMeta dispatcherMeta(Map.Entry<String, Dispatcher> dispatcher) {
74          // sisu components are lazy!
75          Dispatcher d = dispatcher.getValue();
76          if (d instanceof DispatcherMeta meta) {
77              return meta;
78          } else {
79              return new DispatcherMeta() {
80                  @Override
81                  public String name() {
82                      return dispatcher.getKey();
83                  }
84  
85                  @Override
86                  public String displayName() {
87                      return dispatcher.getKey() + " (needs manual configuration)";
88                  }
89  
90                  @Override
91                  public Collection<Field> fields() {
92                      return List.of();
93                  }
94              };
95          }
96      }
97  
98      @Override
99      public String encrypt(String str, Map<String, String> attr) throws SecDispatcherException, IOException {
100         if (isEncryptedString(str)) return str;
101         if (attr == null) {
102             attr = new HashMap<>();
103         } else {
104             attr = new HashMap<>(attr);
105         }
106         if (attr.get(DISPATCHER_NAME_ATTR) == null) {
107             SettingsSecurity conf = readConfiguration(false);
108             if (conf == null) {
109                 throw new SecDispatcherException("No configuration found");
110             }
111             String defaultDispatcher = conf.getDefaultDispatcher();
112             if (defaultDispatcher == null) {
113                 throw new SecDispatcherException("No defaultDispatcher set in configuration");
114             }
115             attr.put(DISPATCHER_NAME_ATTR, defaultDispatcher);
116         }
117         String name = attr.get(DISPATCHER_NAME_ATTR);
118         Dispatcher dispatcher = dispatchers.get(name);
119         if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
120         Dispatcher.EncryptPayload payload = dispatcher.encrypt(str, attr, prepareDispatcherConfig(name));
121         HashMap<String, String> resultAttributes = new HashMap<>(payload.getAttributes());
122         resultAttributes.put(SecDispatcher.DISPATCHER_NAME_ATTR, name);
123         resultAttributes.put(SecDispatcher.DISPATCHER_VERSION_ATTR, SecUtil.specVersion());
124         return SHIELD_BEGIN
125                 + ATTR_START
126                 + resultAttributes.entrySet().stream()
127                         .map(e -> e.getKey() + "=" + e.getValue())
128                         .collect(Collectors.joining(","))
129                 + ATTR_STOP
130                 + payload.getEncrypted()
131                 + SHIELD_END;
132     }
133 
134     @Override
135     public String decrypt(String str) throws SecDispatcherException, IOException {
136         String bare;
137         Map<String, String> attr;
138         if (isLegacyEncryptedString(str)) {
139             bare = unDecorateLegacy(str);
140             attr = new HashMap<>();
141             attr.put(DISPATCHER_NAME_ATTR, LegacyDispatcher.NAME);
142         } else if (isEncryptedString(str)) {
143             bare = unDecorate(str);
144             attr = requireNonNull(stripAttributes(bare));
145         } else {
146             return str;
147         }
148         String name = attr.get(DISPATCHER_NAME_ATTR);
149         Dispatcher dispatcher = dispatchers.get(name);
150         if (dispatcher == null) throw new SecDispatcherException("No dispatcher exist with name " + name);
151         return dispatcher.decrypt(strip(bare), attr, prepareDispatcherConfig(name));
152     }
153 
154     /**
155      * <ul>
156      *     <li>Current: {[name=master,cipher=AES/GCM/NoPadding,version=4.0]vvq66pZ7rkvzSPStGTI9q4QDnsmuDwo+LtjraRel2b0XpcGJFdXcYAHAS75HUA6GLpcVtEkmyQ==}</li>
157      * </ul>
158      */
159     @Override
160     public boolean isEncryptedString(String str) {
161         boolean looksLike = str != null
162                 && !str.isBlank()
163                 && str.startsWith(SHIELD_BEGIN)
164                 && str.endsWith(SHIELD_END)
165                 && !unDecorate(str).contains(SHIELD_BEGIN)
166                 && !unDecorate(str).contains(SHIELD_END);
167         if (looksLike) {
168             Map<String, String> attributes = stripAttributes(unDecorate(str));
169             return attributes.containsKey(DISPATCHER_NAME_ATTR) && attributes.containsKey(DISPATCHER_VERSION_ATTR);
170         }
171         return false;
172     }
173 
174     /**
175      * <ul>
176      *     <li>Legacy: {jSMOWnoPFgsHVpMvz5VrIt5kRbzGpI8u+9EF1iFQyJQ=}</li>
177      * </ul>
178      */
179     @Override
180     public boolean isLegacyEncryptedString(String str) {
181         if (str != null && str.contains(SHIELD_BEGIN)) {
182             str = str.substring(str.indexOf(SHIELD_BEGIN));
183             if (str.contains(SHIELD_END)) {
184                 str = str.substring(0, str.indexOf(SHIELD_END) + 1);
185                 String undecorated = unDecorate(str);
186                 boolean looksLike = !str.isBlank()
187                         && str.startsWith(SHIELD_BEGIN)
188                         && str.endsWith(SHIELD_END)
189                         && !undecorated.contains(SHIELD_BEGIN)
190                         && !undecorated.contains(SHIELD_END);
191                 if (looksLike) {
192                     return stripAttributes(undecorated).isEmpty();
193                 }
194             }
195         }
196         return false;
197     }
198 
199     @Override
200     public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException {
201         SettingsSecurity configuration = SecUtil.read(configurationFile);
202         if (configuration == null && createIfMissing) {
203             configuration = new SettingsSecurity();
204         }
205         return configuration;
206     }
207 
208     @Override
209     public void writeConfiguration(SettingsSecurity configuration) throws IOException {
210         requireNonNull(configuration, "configuration is null");
211         SecUtil.write(configurationFile, configuration, true);
212     }
213 
214     @Override
215     public ValidationResponse validateConfiguration() {
216         HashMap<ValidationResponse.Level, List<String>> report = new HashMap<>();
217         ArrayList<ValidationResponse> subsystems = new ArrayList<>();
218         boolean valid = false;
219         try {
220             SettingsSecurity config = readConfiguration(false);
221             if (config == null) {
222                 report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
223                         .add("No configuration file found on path " + configurationFile);
224             } else {
225                 report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
226                         .add("Configuration file present on path " + configurationFile);
227                 String defaultDispatcher = config.getDefaultDispatcher();
228                 if (defaultDispatcher == null) {
229                     report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
230                             .add("No default dispatcher set in configuration");
231                 } else {
232                     report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
233                             .add("Default dispatcher configured");
234                     Dispatcher dispatcher = dispatchers.get(defaultDispatcher);
235                     if (dispatcher == null) {
236                         report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
237                                 .add("Configured default dispatcher not present in system");
238                     } else {
239                         ValidationResponse dispatcherResponse =
240                                 dispatcher.validateConfiguration(prepareDispatcherConfig(defaultDispatcher));
241                         subsystems.add(dispatcherResponse);
242                         if (!dispatcherResponse.isValid()) {
243                             report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
244                                     .add("Configured default dispatcher configuration is invalid");
245                         } else {
246                             valid = true;
247                             report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
248                                     .add("Configured default dispatcher configuration is valid");
249                         }
250                     }
251                 }
252             }
253 
254             // below is legacy check, that does not affect validity of config, is merely informational
255             Dispatcher legacy = dispatchers.get(LegacyDispatcher.NAME);
256             if (legacy == null) {
257                 report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
258                         .add("Legacy dispatcher not present in system");
259             } else {
260                 report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
261                         .add("Legacy dispatcher present in system");
262                 ValidationResponse legacyResponse =
263                         legacy.validateConfiguration(prepareDispatcherConfig(LegacyDispatcher.NAME));
264                 subsystems.add(legacyResponse);
265                 if (!legacyResponse.isValid()) {
266                     report.computeIfAbsent(ValidationResponse.Level.WARNING, k -> new ArrayList<>())
267                             .add("Legacy dispatcher not operational; transparent fallback not possible");
268                 } else {
269                     report.computeIfAbsent(ValidationResponse.Level.INFO, k -> new ArrayList<>())
270                             .add("Legacy dispatcher is operational; transparent fallback possible");
271                 }
272             }
273         } catch (IOException e) {
274             report.computeIfAbsent(ValidationResponse.Level.ERROR, k -> new ArrayList<>())
275                     .add(e.getMessage());
276         }
277 
278         return new ValidationResponse(getClass().getSimpleName(), valid, report, subsystems);
279     }
280 
281     protected Map<String, String> prepareDispatcherConfig(String name) throws IOException {
282         HashMap<String, String> dispatcherConf = new HashMap<>();
283         Map<String, String> conf = SecUtil.getConfig(SecUtil.read(configurationFile), name);
284         if (conf != null) {
285             dispatcherConf.putAll(conf);
286         }
287         return dispatcherConf;
288     }
289 
290     protected String strip(String str) {
291         int start = str.indexOf(ATTR_START);
292         int stop = str.indexOf(ATTR_STOP);
293         if (start != -1 && stop != -1 && stop > start) {
294             return str.substring(stop + 1);
295         }
296         return str;
297     }
298 
299     protected Map<String, String> stripAttributes(String str) {
300         HashMap<String, String> result = new HashMap<>();
301         int start = str.indexOf(ATTR_START);
302         int stop = str.indexOf(ATTR_STOP);
303         if (start != -1 && stop != -1 && stop > start) {
304             if (start != 0) throw new SecDispatcherException("Attributes can be prefix only");
305             if (stop == start + 1) return null;
306             String attrs = str.substring(start + 1, stop).trim();
307             if (attrs.isEmpty()) return null;
308             StringTokenizer st = new StringTokenizer(attrs, ",");
309             while (st.hasMoreTokens()) {
310                 String pair = st.nextToken();
311                 int pos = pair.indexOf('=');
312                 if (pos == -1) throw new SecDispatcherException("Attribute malformed: " + pair);
313                 String key = pair.substring(0, pos).trim();
314                 String val = pair.substring(pos + 1).trim();
315                 result.put(key, val);
316             }
317         }
318         return result;
319     }
320 
321     protected String unDecorate(String str) {
322         return str.substring(SHIELD_BEGIN.length(), str.length() - SHIELD_END.length());
323     }
324 
325     protected String unDecorateLegacy(String str) {
326         str = str.substring(str.indexOf(SHIELD_BEGIN));
327         str = str.substring(0, str.indexOf(SHIELD_END) + 1);
328         return unDecorate(str);
329     }
330 }