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.syncope.client.lib;
20  
21  import com.fasterxml.jackson.core.type.TypeReference;
22  import com.fasterxml.jackson.databind.json.JsonMapper;
23  import java.io.IOException;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.Set;
29  import javax.ws.rs.core.EntityTag;
30  import javax.ws.rs.core.HttpHeaders;
31  import javax.ws.rs.core.MediaType;
32  import javax.ws.rs.core.Response;
33  import org.apache.commons.lang3.tuple.Triple;
34  import org.apache.cxf.configuration.jsse.TLSClientParameters;
35  import org.apache.cxf.jaxrs.client.Client;
36  import org.apache.cxf.jaxrs.client.ClientConfiguration;
37  import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
38  import org.apache.cxf.jaxrs.client.WebClient;
39  import org.apache.cxf.transport.common.gzip.GZIPInInterceptor;
40  import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor;
41  import org.apache.cxf.transport.http.HTTPConduit;
42  import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit;
43  import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
44  import org.apache.syncope.client.lib.batch.BatchRequest;
45  import org.apache.syncope.common.lib.SyncopeConstants;
46  import org.apache.syncope.common.lib.search.AnyObjectFiqlSearchConditionBuilder;
47  import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder;
48  import org.apache.syncope.common.lib.search.GroupFiqlSearchConditionBuilder;
49  import org.apache.syncope.common.lib.search.OrderByClauseBuilder;
50  import org.apache.syncope.common.lib.search.UserFiqlSearchConditionBuilder;
51  import org.apache.syncope.common.lib.to.UserTO;
52  import org.apache.syncope.common.rest.api.Preference;
53  import org.apache.syncope.common.rest.api.RESTHeaders;
54  import org.apache.syncope.common.rest.api.service.AccessTokenService;
55  import org.apache.syncope.common.rest.api.service.AnyService;
56  import org.apache.syncope.common.rest.api.service.ExecutableService;
57  import org.apache.syncope.common.rest.api.service.UserSelfService;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Entry point for client access to all REST services exposed by Syncope core; obtain instances via
63   * {@link SyncopeClientFactoryBean}.
64   */
65  public class SyncopeClient {
66  
67      protected static final Logger LOG = LoggerFactory.getLogger(SyncopeClient.class);
68  
69      protected static final String HEADER_SPLIT_PROPERTY = "org.apache.cxf.http.header.split";
70  
71      protected static final JsonMapper MAPPER = JsonMapper.builder().findAndAddModules().build();
72  
73      protected final MediaType mediaType;
74  
75      protected final JAXRSClientFactoryBean restClientFactory;
76  
77      protected final RestClientExceptionMapper exceptionMapper;
78  
79      protected final boolean useCompression;
80  
81      protected final HTTPClientPolicy httpClientPolicy;
82  
83      protected final TLSClientParameters tlsClientParameters;
84  
85      public SyncopeClient(
86              final MediaType mediaType,
87              final JAXRSClientFactoryBean restClientFactory,
88              final RestClientExceptionMapper exceptionMapper,
89              final AuthenticationHandler authHandler,
90              final boolean useCompression,
91              final HTTPClientPolicy httpClientPolicy,
92              final TLSClientParameters tlsClientParameters) {
93  
94          this.mediaType = mediaType;
95          this.restClientFactory = restClientFactory;
96          if (this.restClientFactory.getHeaders() == null) {
97              this.restClientFactory.setHeaders(new HashMap<>());
98          }
99          this.exceptionMapper = exceptionMapper;
100         this.useCompression = useCompression;
101         this.httpClientPolicy = httpClientPolicy;
102         this.tlsClientParameters = tlsClientParameters;
103 
104         init(authHandler);
105     }
106 
107     /**
108      * Initializes the provided {@code restClientFactory} with the authentication capabilities of the provided
109      * {@code handler}.
110      *
111      * Currently supports:
112      * <ul>
113      * <li>{@link JWTAuthenticationHandler}</li>
114      * <li>{@link AnonymousAuthenticationHandler}</li>
115      * <li>{@link BasicAuthenticationHandler}</li>
116      * </ul>
117      * More can be supported by subclasses.
118      *
119      * @param authHandler authentication handler
120      */
121     protected void init(final AuthenticationHandler authHandler) {
122         cleanup();
123 
124         if (authHandler instanceof AnonymousAuthenticationHandler) {
125             restClientFactory.setUsername(((AnonymousAuthenticationHandler) authHandler).getUsername());
126             restClientFactory.setPassword(((AnonymousAuthenticationHandler) authHandler).getPassword());
127         } else if (authHandler instanceof BasicAuthenticationHandler) {
128             restClientFactory.setUsername(((BasicAuthenticationHandler) authHandler).getUsername());
129             restClientFactory.setPassword(((BasicAuthenticationHandler) authHandler).getPassword());
130 
131             String jwt = getService(AccessTokenService.class).login().getHeaderString(RESTHeaders.TOKEN);
132             restClientFactory.getHeaders().put(HttpHeaders.AUTHORIZATION, List.of("Bearer " + jwt));
133 
134             restClientFactory.setUsername(null);
135             restClientFactory.setPassword(null);
136         } else if (authHandler instanceof JWTAuthenticationHandler) {
137             restClientFactory.getHeaders().put(
138                     HttpHeaders.AUTHORIZATION,
139                     List.of("Bearer " + ((JWTAuthenticationHandler) authHandler).getJwt()));
140         }
141     }
142 
143     protected void cleanup() {
144         restClientFactory.getHeaders().remove(HttpHeaders.AUTHORIZATION);
145         restClientFactory.getHeaders().remove(RESTHeaders.DELEGATED_BY);
146         restClientFactory.setUsername(null);
147         restClientFactory.setPassword(null);
148     }
149 
150     /**
151      * Gives the base address for REST calls.
152      *
153      * @return the base address for REST calls
154      */
155     public String getAddress() {
156         return restClientFactory.getAddress();
157     }
158 
159     /**
160      * Requests to invoke services as delegating user.
161      *
162      * @param delegating delegating username
163      * @return this instance, for fluent usage
164      */
165     public SyncopeClient delegatedBy(final String delegating) {
166         if (delegating == null) {
167             restClientFactory.getHeaders().remove(RESTHeaders.DELEGATED_BY);
168         } else {
169             restClientFactory.getHeaders().put(RESTHeaders.DELEGATED_BY, List.of(delegating));
170         }
171         return this;
172     }
173 
174     /**
175      * Attempts to extend the lifespan of the JWT currently in use.
176      */
177     public void refresh() {
178         String jwt = getService(AccessTokenService.class).refresh().getHeaderString(RESTHeaders.TOKEN);
179         restClientFactory.getHeaders().put(HttpHeaders.AUTHORIZATION, List.of("Bearer " + jwt));
180     }
181 
182     /**
183      * Invalidates the JWT currently in use.
184      */
185     public void logout() {
186         try {
187             getService(AccessTokenService.class).logout();
188         } catch (Exception e) {
189             LOG.error("While logging out, cleaning up anyway", e);
190         }
191         cleanup();
192     }
193 
194     /**
195      * (Re)initializes the current instance with the authentication capabilities of the provided {@code handler}.
196      *
197      * @param handler authentication handler
198      */
199     public void login(final AuthenticationHandler handler) {
200         init(handler);
201     }
202 
203     /**
204      * Returns a new instance of {@link UserFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
205      *
206      * @return default instance of {@link UserFiqlSearchConditionBuilder}
207      */
208     public static UserFiqlSearchConditionBuilder getUserSearchConditionBuilder() {
209         return new UserFiqlSearchConditionBuilder();
210     }
211 
212     /**
213      * Returns a new instance of {@link GroupFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
214      *
215      * @return default instance of {@link GroupFiqlSearchConditionBuilder}
216      */
217     public static GroupFiqlSearchConditionBuilder getGroupSearchConditionBuilder() {
218         return new GroupFiqlSearchConditionBuilder();
219     }
220 
221     /**
222      * Returns a new instance of {@link AnyObjectFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
223      *
224      * @param type any type
225      * @return default instance of {@link AnyObjectFiqlSearchConditionBuilder}
226      */
227     public static AnyObjectFiqlSearchConditionBuilder getAnyObjectSearchConditionBuilder(final String type) {
228         return new AnyObjectFiqlSearchConditionBuilder(type);
229     }
230 
231     /**
232      * Returns a new instance of {@link ConnObjectTOFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
233      *
234      * @return default instance of {@link ConnObjectTOFiqlSearchConditionBuilder}
235      */
236     public static ConnObjectTOFiqlSearchConditionBuilder getConnObjectTOFiqlSearchConditionBuilder() {
237         return new ConnObjectTOFiqlSearchConditionBuilder();
238     }
239 
240     /**
241      * Returns a new instance of {@link OrderByClauseBuilder}, for assisted building of {@code orderby} clauses.
242      *
243      * @return default instance of {@link OrderByClauseBuilder}
244      */
245     public static OrderByClauseBuilder getOrderByClauseBuilder() {
246         return new OrderByClauseBuilder();
247     }
248 
249     /**
250      * Returns the JWT in used by this instance, passed with the {@link HttpHeaders#AUTHORIZATION} header
251      * in all requests. It can be null (in case {@link AnonymousAuthenticationHandler} was used).
252      *
253      * @return the JWT in used by this instance
254      */
255     public String getJWT() {
256         List<String> headerValues = restClientFactory.getHeaders().get(HttpHeaders.AUTHORIZATION);
257         String header = headerValues == null || headerValues.isEmpty()
258                 ? null
259                 : headerValues.get(0);
260         if (header != null && header.startsWith("Bearer ")) {
261             return header.substring("Bearer ".length());
262 
263         }
264         return null;
265     }
266 
267     /**
268      * Returns the domain configured for this instance, or {@link SyncopeConstants#MASTER_DOMAIN} if not set.
269      *
270      * @return the domain configured for this instance
271      */
272     public String getDomain() {
273         List<String> headerValues = restClientFactory.getHeaders().get(RESTHeaders.DOMAIN);
274         return headerValues == null || headerValues.isEmpty()
275                 ? SyncopeConstants.MASTER_DOMAIN
276                 : headerValues.get(0);
277     }
278 
279     /**
280      * Creates an instance of the given service class, with configured content type and authentication.
281      *
282      * @param <T> any service class
283      * @param serviceClass service class reference
284      * @return service instance of the given reference class
285      */
286     public <T> T getService(final Class<T> serviceClass) {
287         T serviceInstance;
288         synchronized (restClientFactory) {
289             restClientFactory.setServiceClass(serviceClass);
290             serviceInstance = restClientFactory.create(serviceClass);
291         }
292 
293         Client client = WebClient.client(serviceInstance);
294         client.type(mediaType).accept(mediaType);
295         if (serviceInstance instanceof AnyService || serviceInstance instanceof ExecutableService) {
296             client.accept(RESTHeaders.MULTIPART_MIXED);
297         }
298 
299         ClientConfiguration config = WebClient.getConfig(client);
300         config.getRequestContext().put(HEADER_SPLIT_PROPERTY, true);
301         config.getRequestContext().put(AsyncHTTPConduit.USE_ASYNC, Boolean.TRUE);
302         if (useCompression) {
303             config.getInInterceptors().add(new GZIPInInterceptor());
304             config.getOutInterceptors().add(new GZIPOutInterceptor());
305         }
306 
307         HTTPConduit httpConduit = (HTTPConduit) config.getConduit();
308         Optional.ofNullable(httpClientPolicy).ifPresent(httpConduit::setClient);
309         Optional.ofNullable(tlsClientParameters).ifPresent(httpConduit::setTlsClientParameters);
310 
311         return serviceInstance;
312     }
313 
314     public Triple<Map<String, Set<String>>, List<String>, UserTO> self() {
315         // Explicitly disable header value split because it interferes with JSON deserialization below
316         UserSelfService service = getService(UserSelfService.class);
317         WebClient.getConfig(WebClient.client(service)).getRequestContext().put(HEADER_SPLIT_PROPERTY, false);
318 
319         Response response = service.read();
320         if (response.getStatusInfo().getStatusCode() != Response.Status.OK.getStatusCode()) {
321             Exception ex = exceptionMapper.fromResponse(response);
322             if (ex != null) {
323                 throw (RuntimeException) ex;
324             }
325         }
326 
327         try {
328             return Triple.of(
329                     MAPPER.readValue(
330                             response.getHeaderString(RESTHeaders.OWNED_ENTITLEMENTS), new TypeReference<>() {
331                     }),
332                     MAPPER.readValue(
333                             response.getHeaderString(RESTHeaders.DELEGATIONS), new TypeReference<>() {
334                     }),
335                     response.readEntity(UserTO.class));
336         } catch (IOException e) {
337             throw new IllegalStateException(e);
338         }
339     }
340 
341     /**
342      * Sets the given header on the give service instance.
343      *
344      * @param <T> any service class
345      * @param service service class instance
346      * @param key HTTP header key
347      * @param values HTTP header values
348      * @return given service instance, with given header set
349      */
350     public static <T> T header(final T service, final String key, final Object... values) {
351         WebClient.client(service).header(key, values);
352         return service;
353     }
354 
355     /**
356      * Sets the {@code Prefer} header on the give service instance.
357      *
358      * @param <T> any service class
359      * @param service service class instance
360      * @param preference preference to be set via {@code Prefer} header
361      * @return given service instance, with {@code Prefer} header set
362      */
363     public static <T> T prefer(final T service, final Preference preference) {
364         return header(service, RESTHeaders.PREFER, preference.toString());
365     }
366 
367     /**
368      * Asks for asynchronous propagation towards external resources with null priority.
369      *
370      * @param <T> any service class
371      * @param service service class instance
372      * @param nullPriorityAsync whether asynchronous propagation towards external resources with null priority is
373      * requested
374      * @return service instance of the given reference class, with related header set
375      */
376     public static <T> T nullPriorityAsync(final T service, final boolean nullPriorityAsync) {
377         return header(service, RESTHeaders.NULL_PRIORITY_ASYNC, nullPriorityAsync);
378     }
379 
380     /**
381      * Sets the {@code If-Match} or {@code If-None-Match} header on the given service instance.
382      *
383      * @param <T> any service class
384      * @param service service class instance
385      * @param etag ETag value
386      * @param ifNot if true then {@code If-None-Match} is set, {@code If-Match} otherwise
387      * @return given service instance, with {@code If-Match} or {@code If-None-Match} set
388      */
389     protected static <T> T match(final T service, final EntityTag etag, final boolean ifNot) {
390         WebClient.client(service).match(etag, ifNot);
391         return service;
392     }
393 
394     /**
395      * Sets the {@code If-Match} header on the given service instance.
396      *
397      * @param <T> any service class
398      * @param service service class instance
399      * @param etag ETag value
400      * @return given service instance, with {@code If-Match} set
401      */
402     public static <T> T ifMatch(final T service, final EntityTag etag) {
403         return match(service, etag, false);
404     }
405 
406     /**
407      * Sets the {@code If-None-Match} header on the given service instance.
408      *
409      * @param <T> any service class
410      * @param service service class instance
411      * @param etag ETag value
412      * @return given service instance, with {@code If-None-Match} set
413      */
414     public static <T> T ifNoneMatch(final T service, final EntityTag etag) {
415         return match(service, etag, true);
416     }
417 
418     /**
419      * Fetches {@code ETag} header value from latest service run (if available).
420      *
421      * @param <T> any service class
422      * @param service service class instance
423      * @return {@code ETag} header value from latest service run (if available)
424      */
425     public static <T> EntityTag getLatestEntityTag(final T service) {
426         return WebClient.client(service).getResponse().getEntityTag();
427     }
428 
429     /**
430      * Initiates a new Batch request.
431      *
432      * The typical operation flow is:
433      * <pre>
434      * BatchRequest batchRequest = syncopeClient.batch();
435      * batchRequest.getService(UserService.class).create(...);
436      * batchRequest.getService(UserService.class).update(...);
437      * batchRequest.getService(GroupService.class).update(...);
438      * batchRequest.getService(GroupService.class).delete(...);
439      * ...
440      * BatchResponse batchResponse = batchRequest().commit();
441      * List&lt;BatchResponseItem&gt; items = batchResponse.getItems()
442      * </pre>
443      *
444      * @return empty Batch request
445      */
446     public BatchRequest batch() {
447         return new BatchRequest(
448                 mediaType,
449                 restClientFactory.getAddress(),
450                 restClientFactory.getProviders(),
451                 getJWT(),
452                 tlsClientParameters);
453     }
454 }