View Javadoc
1   package io.apicurio.registry.maven;
2   
3   import com.microsoft.kiota.ApiException;
4   import io.apicurio.registry.client.RegistryClientFactory;
5   import io.apicurio.registry.client.common.RegistryClientOptions;
6   import io.apicurio.registry.rest.client.RegistryClient;
7   import io.apicurio.registry.rest.client.models.ProblemDetails;
8   import io.apicurio.registry.rest.client.models.RuleViolationProblemDetails;
9   import io.apicurio.registry.types.ContentTypes;
10  import io.vertx.core.Vertx;
11  import io.vertx.core.VertxOptions;
12  import io.vertx.core.file.FileSystemOptions;
13  import org.apache.maven.plugin.AbstractMojo;
14  import org.apache.maven.plugin.MojoExecutionException;
15  import org.apache.maven.plugin.MojoFailureException;
16  import org.apache.maven.plugins.annotations.Parameter;
17  
18  import java.util.Locale;
19  import java.util.concurrent.ExecutionException;
20  
21  /**
22   * Base class for all Registry Mojo's. It handles RegistryService's (aka client) lifecycle.
23   */
24  public abstract class AbstractRegistryMojo extends AbstractMojo {
25  
26      /**
27       * The registry's url. e.g. http://localhost:8080/apis/registry/v3
28       */
29      @Parameter(required = true, property = "apicurio.url")
30      String registryUrl;
31  
32      /**
33       * The URL of the authentication server (if required).
34       */
35      @Parameter(property = "auth.server.url")
36      String authServerUrl;
37  
38      /**
39       * The client id to use when authenticating to the auth sever.
40       */
41      @Parameter(property = "client.id")
42      String clientId;
43  
44      /**
45       * The client secret to use when authenticating to the auth sever.
46       */
47      @Parameter(property = "client.secret")
48      String clientSecret;
49  
50      /**
51       * The client scope to use when authenticating to the auth sever.
52       */
53      @Parameter(property = "client.scope")
54      String clientScope;
55  
56      /**
57       * Authentication credentials: username
58       */
59      @Parameter(property = "username")
60      String username;
61  
62      /**
63       * Authentication credentials: password
64       */
65      @Parameter(property = "password")
66      String password;
67  
68      /**
69       * Path to the trust store file for SSL/TLS connections.
70       * Supports JKS, PKCS12, and PEM formats. The format is auto-detected from the file extension
71       * (.jks, .p12/.pfx, .pem/.crt/.cer) unless explicitly specified via trustStoreType.
72       */
73      @Parameter(property = "trustStorePath")
74      String trustStorePath;
75  
76      /**
77       * Password for the trust store (required for JKS and PKCS12 formats).
78       */
79      @Parameter(property = "trustStorePassword")
80      String trustStorePassword;
81  
82      /**
83       * Type of the trust store: JKS, PKCS12, or PEM. If not specified, the type is auto-detected
84       * from the file extension.
85       */
86      @Parameter(property = "trustStoreType")
87      String trustStoreType;
88  
89      /**
90       * Path to the key store file for mutual TLS (mTLS) client authentication.
91       * Supports JKS, PKCS12, and PEM formats. The format is auto-detected from the file extension
92       * (.jks, .p12/.pfx, .pem/.crt/.cer) unless explicitly specified via keyStoreType.
93       */
94      @Parameter(property = "keyStorePath")
95      String keyStorePath;
96  
97      /**
98       * Password for the key store (required for JKS and PKCS12 formats).
99       */
100     @Parameter(property = "keyStorePassword")
101     String keyStorePassword;
102 
103     /**
104      * Type of the key store: JKS, PKCS12, or PEM. If not specified, the type is auto-detected
105      * from the file extension.
106      */
107     @Parameter(property = "keyStoreType")
108     String keyStoreType;
109 
110     /**
111      * Path to the PEM private key file (required when using PEM format for mTLS and keyStorePath
112      * points to the certificate file).
113      */
114     @Parameter(property = "keyStorePemKeyPath")
115     String keyStorePemKeyPath;
116 
117     /**
118      * If set to true, trust all SSL/TLS certificates without validation.
119      * WARNING: This should only be used for development/testing purposes.
120      */
121     @Parameter(property = "trustAll", defaultValue = "false")
122     boolean trustAll;
123 
124     /**
125      * If set to false, disable hostname verification in SSL/TLS connections.
126      * WARNING: Disabling this reduces security and should only be used for development/testing.
127      */
128     @Parameter(property = "verifyHostname", defaultValue = "true")
129     boolean verifyHostname;
130 
131     protected Vertx createVertx() {
132         var options = new VertxOptions();
133         var fsOpts = new FileSystemOptions();
134         fsOpts.setFileCachingEnabled(false);
135         fsOpts.setClassPathResolvingEnabled(false);
136         options.setFileSystemOptions(fsOpts);
137         return Vertx.vertx(options);
138     }
139 
140     protected RegistryClient createClient(Vertx vertx) {
141         RegistryClientOptions clientOptions = RegistryClientOptions.create(registryUrl, vertx);
142 
143         // Configure authentication
144         if (authServerUrl != null && clientId != null && clientSecret != null) {
145             if (clientScope != null && !clientScope.isEmpty()) {
146                 getLog().info("Creating registry client with OAuth2 authentication with scope.");
147                 clientOptions.oauth2(authServerUrl, clientId, clientSecret, clientScope);
148             } else {
149                 getLog().info("Creating registry client with OAuth2 authentication.");
150                 clientOptions.oauth2(authServerUrl, clientId, clientSecret);
151             }
152         } else if (username != null && password != null) {
153             getLog().info("Creating registry client with Basic authentication.");
154             clientOptions.basicAuth(username, password);
155         } else {
156             getLog().info("Creating registry client without authentication.");
157         }
158 
159         // Configure TLS/SSL trust store
160         configureTrustStore(clientOptions);
161 
162         // Configure TLS/SSL key store for mTLS
163         configureKeyStore(clientOptions);
164 
165         // Configure additional SSL options
166         if (trustAll) {
167             getLog().warn("TLS trust-all mode enabled. This is insecure and should only be used for development/testing.");
168             clientOptions.trustAll(true);
169         }
170         if (!verifyHostname) {
171             getLog().warn("Hostname verification disabled. This reduces security and should only be used for development/testing.");
172             clientOptions.verifyHost(false);
173         }
174 
175         return RegistryClientFactory.create(clientOptions.retry());
176     }
177 
178     // Package-private for testing
179     void configureTrustStore(RegistryClientOptions clientOptions) {
180         if (trustStorePath == null || trustStorePath.isEmpty()) {
181             return;
182         }
183 
184         String detectedType = detectStoreType(trustStorePath, trustStoreType);
185         getLog().info("Configuring trust store: " + trustStorePath + " (type: " + detectedType + ")");
186 
187         switch (detectedType.toUpperCase(Locale.ROOT)) {
188             case "JKS":
189                 clientOptions.trustStoreJks(trustStorePath, trustStorePassword);
190                 break;
191             case "PKCS12":
192                 clientOptions.trustStorePkcs12(trustStorePath, trustStorePassword);
193                 break;
194             case "PEM":
195                 clientOptions.trustStorePem(trustStorePath);
196                 break;
197             default:
198                 getLog().warn("Unknown trust store type: " + detectedType + ". Attempting to use as PKCS12.");
199                 clientOptions.trustStorePkcs12(trustStorePath, trustStorePassword);
200         }
201     }
202 
203     // Package-private for testing
204     void configureKeyStore(RegistryClientOptions clientOptions) {
205         if (keyStorePath == null || keyStorePath.isEmpty()) {
206             return;
207         }
208 
209         String detectedType = detectStoreType(keyStorePath, keyStoreType);
210         getLog().info("Configuring key store for mTLS: " + keyStorePath + " (type: " + detectedType + ")");
211 
212         switch (detectedType.toUpperCase(Locale.ROOT)) {
213             case "JKS":
214                 clientOptions.keystoreJks(keyStorePath, keyStorePassword);
215                 break;
216             case "PKCS12":
217                 clientOptions.keystorePkcs12(keyStorePath, keyStorePassword);
218                 break;
219             case "PEM":
220                 if (keyStorePemKeyPath == null || keyStorePemKeyPath.isEmpty()) {
221                     throw new IllegalArgumentException(
222                             "keyStorePemKeyPath is required when using PEM format for mTLS. " +
223                             "Set keyStorePath to the certificate file and keyStorePemKeyPath to the private key file.");
224                 }
225                 clientOptions.keystorePem(keyStorePath, keyStorePemKeyPath);
226                 break;
227             default:
228                 getLog().warn("Unknown key store type: " + detectedType + ". Attempting to use as PKCS12.");
229                 clientOptions.keystorePkcs12(keyStorePath, keyStorePassword);
230         }
231     }
232 
233     // Package-private for testing
234     String detectStoreType(String path, String explicitType) {
235         if (explicitType != null && !explicitType.isEmpty()) {
236             return explicitType;
237         }
238         String lowerPath = path.toLowerCase(Locale.ROOT);
239         if (lowerPath.endsWith(".jks")) {
240             return "JKS";
241         } else if (lowerPath.endsWith(".p12") || lowerPath.endsWith(".pfx")) {
242             return "PKCS12";
243         } else if (lowerPath.endsWith(".pem") || lowerPath.endsWith(".crt") || lowerPath.endsWith(".cer")) {
244             return "PEM";
245         }
246         return "PKCS12"; // Default to PKCS12
247     }
248 
249     @Override
250     public void execute() throws MojoExecutionException, MojoFailureException {
251         try {
252             executeInternal();
253         } catch (ExecutionException e) {
254             throw new MojoExecutionException(e);
255         } catch (InterruptedException e) {
256             throw new MojoFailureException(e);
257         }
258         closeClients();
259     }
260 
261     private void closeClients() {
262         // TODO: check there are no connection leaks etc...
263     }
264 
265     protected abstract void executeInternal()
266             throws MojoExecutionException, MojoFailureException, ExecutionException, InterruptedException;
267 
268     protected String getContentTypeByExtension(String fileName) {
269         if (fileName == null)
270             return null;
271         String[] temp = fileName.split("[.]");
272         String extension = temp[temp.length - 1];
273         switch (extension.toLowerCase(Locale.ROOT)) {
274             case "avro":
275             case "avsc":
276             case "json":
277                 return ContentTypes.APPLICATION_JSON;
278             case "yml":
279             case "yaml":
280                 return ContentTypes.APPLICATION_YAML;
281             case "graphql":
282                 return ContentTypes.APPLICATION_GRAPHQL;
283             case "proto":
284                 return ContentTypes.APPLICATION_PROTOBUF;
285             case "wsdl":
286             case "xsd":
287             case "xml":
288                 return ContentTypes.APPLICATION_XML;
289         }
290         return null;
291     }
292 
293     public void setRegistryUrl(String registryUrl) {
294         this.registryUrl = registryUrl;
295     }
296 
297     public void setAuthServerUrl(String authServerUrl) {
298         this.authServerUrl = authServerUrl;
299     }
300 
301     public void setClientId(String clientId) {
302         this.clientId = clientId;
303     }
304 
305     public void setClientSecret(String clientSecret) {
306         this.clientSecret = clientSecret;
307     }
308 
309     public void setClientScope(String clientScope) {
310         this.clientScope = clientScope;
311     }
312 
313     public void setUsername(String username) {
314         this.username = username;
315     }
316 
317     public void setPassword(String password) {
318         this.password = password;
319     }
320 
321     public void setTrustStorePath(String trustStorePath) {
322         this.trustStorePath = trustStorePath;
323     }
324 
325     public void setTrustStorePassword(String trustStorePassword) {
326         this.trustStorePassword = trustStorePassword;
327     }
328 
329     public void setTrustStoreType(String trustStoreType) {
330         this.trustStoreType = trustStoreType;
331     }
332 
333     public void setKeyStorePath(String keyStorePath) {
334         this.keyStorePath = keyStorePath;
335     }
336 
337     public void setKeyStorePassword(String keyStorePassword) {
338         this.keyStorePassword = keyStorePassword;
339     }
340 
341     public void setKeyStoreType(String keyStoreType) {
342         this.keyStoreType = keyStoreType;
343     }
344 
345     public void setKeyStorePemKeyPath(String keyStorePemKeyPath) {
346         this.keyStorePemKeyPath = keyStorePemKeyPath;
347     }
348 
349     public void setTrustAll(boolean trustAll) {
350         this.trustAll = trustAll;
351     }
352 
353     public void setVerifyHostname(boolean verifyHostname) {
354         this.verifyHostname = verifyHostname;
355     }
356 
357     protected void logAndThrow(ApiException e) throws MojoExecutionException, MojoFailureException {
358         if (e instanceof RuleViolationProblemDetails) {
359             logAndThrow((RuleViolationProblemDetails) e);
360         }
361         if (e instanceof ProblemDetails) {
362             logAndThrow((ProblemDetails) e);
363         }
364     }
365 
366     protected void logAndThrow(ProblemDetails e) throws MojoExecutionException {
367         getLog().error("---");
368         getLog().error("Error registering artifact: " + e.getName());
369         getLog().error(e.getTitle());
370         getLog().error(e.getDetail());
371         getLog().error("---");
372         throw new MojoExecutionException("Error registering artifact: " + e.getName(), e);
373     }
374 
375     protected void logAndThrow(RuleViolationProblemDetails e) throws MojoFailureException {
376         getLog().error("---");
377         getLog().error("Registry rule validation failure: " + e.getName());
378         getLog().error(e.getTitle());
379         if (e.getCauses() != null) {
380             e.getCauses().forEach(cause -> {
381                 getLog().error("\t-> " + cause.getContext());
382                 getLog().error("\t   " + cause.getDescription());
383             });
384         }
385         getLog().error("---");
386         throw new MojoFailureException("Registry rule validation failure: " + e.getName(), e);
387     }
388 
389 }