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.core.rest.cxf.service;
20  
21  import java.time.OffsetDateTime;
22  import java.util.List;
23  import java.util.Optional;
24  import java.util.Set;
25  import java.util.stream.Collectors;
26  import javax.ws.rs.BadRequestException;
27  import javax.ws.rs.core.Response;
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.commons.lang3.exception.ExceptionUtils;
30  import org.apache.commons.lang3.tuple.Pair;
31  import org.apache.syncope.common.lib.Attr;
32  import org.apache.syncope.common.lib.SyncopeClientException;
33  import org.apache.syncope.common.lib.SyncopeConstants;
34  import org.apache.syncope.common.lib.request.AnyCR;
35  import org.apache.syncope.common.lib.request.AnyUR;
36  import org.apache.syncope.common.lib.request.AttrPatch;
37  import org.apache.syncope.common.lib.request.ResourceAR;
38  import org.apache.syncope.common.lib.request.ResourceDR;
39  import org.apache.syncope.common.lib.to.AnyTO;
40  import org.apache.syncope.common.lib.to.PagedResult;
41  import org.apache.syncope.common.lib.to.ProvisioningResult;
42  import org.apache.syncope.common.lib.types.ClientExceptionType;
43  import org.apache.syncope.common.lib.types.PatchOperation;
44  import org.apache.syncope.common.lib.types.ResourceAssociationAction;
45  import org.apache.syncope.common.lib.types.ResourceDeassociationAction;
46  import org.apache.syncope.common.lib.types.SchemaType;
47  import org.apache.syncope.common.rest.api.Preference;
48  import org.apache.syncope.common.rest.api.RESTHeaders;
49  import org.apache.syncope.common.rest.api.batch.BatchPayloadGenerator;
50  import org.apache.syncope.common.rest.api.batch.BatchResponseItem;
51  import org.apache.syncope.common.rest.api.beans.AnyQuery;
52  import org.apache.syncope.common.rest.api.service.AnyService;
53  import org.apache.syncope.common.rest.api.service.JAXRSService;
54  import org.apache.syncope.core.logic.AbstractAnyLogic;
55  import org.apache.syncope.core.persistence.api.dao.AnyDAO;
56  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
57  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
58  import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
59  import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
60  import org.apache.syncope.core.spring.security.SecureRandomUtils;
61  
62  public abstract class AbstractAnyService<TO extends AnyTO, CR extends AnyCR, UR extends AnyUR>
63          extends AbstractSearchService implements AnyService<TO> {
64  
65      public AbstractAnyService(final SearchCondVisitor searchCondVisitor) {
66          super(searchCondVisitor);
67      }
68  
69      protected abstract AnyDAO<?> getAnyDAO();
70  
71      protected abstract AbstractAnyLogic<TO, CR, UR> getAnyLogic();
72  
73      protected abstract UR newUpdateReq(String key);
74  
75      @Override
76      public Set<Attr> read(final String key, final SchemaType schemaType) {
77          TO any = read(key);
78          Set<Attr> result;
79          switch (schemaType) {
80              case DERIVED:
81                  result = any.getDerAttrs();
82                  break;
83  
84              case VIRTUAL:
85                  result = any.getVirAttrs();
86                  break;
87  
88              case PLAIN:
89              default:
90                  result = any.getPlainAttrs();
91          }
92  
93          return result;
94      }
95  
96      @Override
97      public Attr read(final String key, final SchemaType schemaType, final String schema) {
98          TO any = read(key);
99          Optional<Attr> result;
100         switch (schemaType) {
101             case DERIVED:
102                 result = any.getDerAttr(schema);
103                 break;
104 
105             case VIRTUAL:
106                 result = any.getVirAttr(schema);
107                 break;
108 
109             case PLAIN:
110             default:
111                 result = any.getPlainAttr(schema);
112         }
113 
114         return result.
115                 orElseThrow(() -> new NotFoundException("Attribute for type " + schemaType + " and schema " + schema));
116     }
117 
118     @Override
119     public TO read(final String key) {
120         return getAnyLogic().read(findActualKey(getAnyDAO(), key));
121     }
122 
123     @Override
124     public PagedResult<TO> search(final AnyQuery anyQuery) {
125         String realm = StringUtils.prependIfMissing(anyQuery.getRealm(), SyncopeConstants.ROOT_REALM);
126 
127         SearchCond searchCond = StringUtils.isBlank(anyQuery.getFiql())
128                 ? null
129                 : getSearchCond(anyQuery.getFiql(), realm);
130 
131         try {
132             Pair<Integer, List<TO>> result = getAnyLogic().search(
133                     searchCond,
134                     anyQuery.getPage(),
135                     anyQuery.getSize(),
136                     getOrderByClauses(anyQuery.getOrderBy()),
137                     realm,
138                     anyQuery.getRecursive(),
139                     anyQuery.getDetails());
140 
141             return buildPagedResult(result.getRight(), anyQuery.getPage(), anyQuery.getSize(), result.getLeft());
142         } catch (IllegalArgumentException e) {
143             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
144             sce.getElements().add(anyQuery.getFiql());
145             sce.getElements().add(ExceptionUtils.getRootCauseMessage(e));
146             throw sce;
147         }
148     }
149 
150     protected OffsetDateTime findLastChange(final String key) {
151         OffsetDateTime lastChange = getAnyDAO().findLastChange(key);
152         if (lastChange == null) {
153             throw new NotFoundException("User, Group or Any Object for " + key);
154         }
155 
156         return lastChange;
157     }
158 
159     protected Response doUpdate(final UR updateReq) {
160         updateReq.setKey(findActualKey(getAnyDAO(), updateReq.getKey()));
161         OffsetDateTime etag = findLastChange(updateReq.getKey());
162         checkETag(String.valueOf(etag.toInstant().toEpochMilli()));
163 
164         ProvisioningResult<TO> updated = getAnyLogic().update(updateReq, isNullPriorityAsync());
165         return modificationResponse(updated);
166     }
167 
168     protected void addUpdateOrReplaceAttr(
169             final String key, final SchemaType schemaType, final Attr attrTO, final PatchOperation operation) {
170 
171         if (attrTO.getSchema() == null) {
172             throw new NotFoundException("Must specify schema");
173         }
174 
175         UR updateReq = newUpdateReq(key);
176 
177         switch (schemaType) {
178             case VIRTUAL:
179                 updateReq.getVirAttrs().add(attrTO);
180                 break;
181 
182             case PLAIN:
183                 updateReq.getPlainAttrs().add(new AttrPatch.Builder(attrTO).operation(operation).build());
184                 break;
185 
186             case DERIVED:
187             default:
188         }
189 
190         doUpdate(updateReq);
191     }
192 
193     @Override
194     public Response update(final String key, final SchemaType schemaType, final Attr attrTO) {
195         String actualKey = findActualKey(getAnyDAO(), key);
196         addUpdateOrReplaceAttr(actualKey, schemaType, attrTO, PatchOperation.ADD_REPLACE);
197         return modificationResponse(read(actualKey, schemaType, attrTO.getSchema()));
198     }
199 
200     @Override
201     public void delete(final String key, final SchemaType schemaType, final String schema) {
202         addUpdateOrReplaceAttr(findActualKey(getAnyDAO(), key),
203                 schemaType,
204                 new Attr.Builder(schema).build(),
205                 PatchOperation.DELETE);
206     }
207 
208     @Override
209     public Response delete(final String key) {
210         String actualKey = findActualKey(getAnyDAO(), key);
211 
212         OffsetDateTime etag = findLastChange(actualKey);
213         checkETag(String.valueOf(etag.toInstant().toEpochMilli()));
214 
215         ProvisioningResult<TO> deleted = getAnyLogic().delete(actualKey, isNullPriorityAsync());
216         return modificationResponse(deleted);
217     }
218 
219     @Override
220     public Response deassociate(final ResourceDR req) {
221         OffsetDateTime etag = findLastChange(req.getKey());
222         checkETag(String.valueOf(etag.toInstant().toEpochMilli()));
223 
224         ProvisioningResult<TO> updated;
225         switch (req.getAction()) {
226             case UNLINK:
227                 updated = new ProvisioningResult<>();
228                 updated.setEntity(getAnyLogic().unlink(req.getKey(), req.getResources()));
229                 break;
230 
231             case UNASSIGN:
232                 updated = getAnyLogic().unassign(req.getKey(), req.getResources(), isNullPriorityAsync());
233                 break;
234 
235             case DEPROVISION:
236                 updated = getAnyLogic().deprovision(req.getKey(), req.getResources(), isNullPriorityAsync());
237                 break;
238 
239             default:
240                 throw new BadRequestException("Missing action");
241         }
242 
243         List<BatchResponseItem> batchResponseItems;
244         if (req.getAction() == ResourceDeassociationAction.UNLINK) {
245             batchResponseItems = req.getResources().stream().map(resource -> {
246                 BatchResponseItem item = new BatchResponseItem();
247 
248                 item.getHeaders().put(RESTHeaders.RESOURCE_KEY, List.of(resource));
249 
250                 item.setStatus(updated.getEntity().getResources().contains(resource)
251                         ? Response.Status.BAD_REQUEST.getStatusCode()
252                         : Response.Status.OK.getStatusCode());
253 
254                 if (getPreference() == Preference.RETURN_NO_CONTENT) {
255                     item.getHeaders().put(
256                             RESTHeaders.PREFERENCE_APPLIED,
257                             List.of(Preference.RETURN_NO_CONTENT.toString()));
258                 } else {
259                     item.setContent(POJOHelper.serialize(updated.getEntity()));
260                 }
261 
262                 return item;
263             }).collect(Collectors.toList());
264         } else {
265             batchResponseItems = updated.getPropagationStatuses().stream().
266                     map(status -> {
267                         BatchResponseItem item = new BatchResponseItem();
268 
269                         item.getHeaders().put(RESTHeaders.RESOURCE_KEY, List.of(status.getResource()));
270 
271                         item.setStatus(status.getStatus().getHttpStatus());
272 
273                         if (status.getFailureReason() != null) {
274                             item.getHeaders().put(RESTHeaders.ERROR_INFO, List.of(status.getFailureReason()));
275                         }
276 
277                         if (getPreference() == Preference.RETURN_NO_CONTENT) {
278                             item.getHeaders().put(
279                                     RESTHeaders.PREFERENCE_APPLIED,
280                                     List.of(Preference.RETURN_NO_CONTENT.toString()));
281                         } else {
282                             item.setContent(POJOHelper.serialize(updated.getEntity()));
283                         }
284 
285                         return item;
286                     }).collect(Collectors.toList());
287         }
288 
289         String boundary = "deassociate_" + SecureRandomUtils.generateRandomUUID().toString();
290         return Response.ok(BatchPayloadGenerator.generate(
291                 batchResponseItems, JAXRSService.DOUBLE_DASH + boundary)).
292                 type(RESTHeaders.multipartMixedWith(boundary)).
293                 build();
294     }
295 
296     @Override
297     public Response associate(final ResourceAR req) {
298         OffsetDateTime etag = findLastChange(req.getKey());
299         checkETag(String.valueOf(etag.toInstant().toEpochMilli()));
300 
301         ProvisioningResult<TO> updated;
302         switch (req.getAction()) {
303             case LINK:
304                 updated = new ProvisioningResult<>();
305                 updated.setEntity(getAnyLogic().link(
306                         req.getKey(),
307                         req.getResources()));
308                 break;
309 
310             case ASSIGN:
311                 updated = getAnyLogic().assign(
312                         req.getKey(),
313                         req.getResources(),
314                         req.getValue() != null,
315                         req.getValue(),
316                         isNullPriorityAsync());
317                 break;
318 
319             case PROVISION:
320                 updated = getAnyLogic().provision(
321                         req.getKey(),
322                         req.getResources(),
323                         req.getValue() != null,
324                         req.getValue(),
325                         isNullPriorityAsync());
326                 break;
327 
328             default:
329                 throw new BadRequestException("Missing action");
330         }
331 
332         List<BatchResponseItem> batchResponseItems;
333         if (req.getAction() == ResourceAssociationAction.LINK) {
334             batchResponseItems = req.getResources().stream().map(resource -> {
335                 BatchResponseItem item = new BatchResponseItem();
336 
337                 item.getHeaders().put(RESTHeaders.RESOURCE_KEY, List.of(resource));
338 
339                 item.setStatus(updated.getEntity().getResources().contains(resource)
340                         ? Response.Status.OK.getStatusCode()
341                         : Response.Status.BAD_REQUEST.getStatusCode());
342 
343                 if (getPreference() == Preference.RETURN_NO_CONTENT) {
344                     item.getHeaders().put(
345                             RESTHeaders.PREFERENCE_APPLIED,
346                             List.of(Preference.RETURN_NO_CONTENT.toString()));
347                 } else {
348                     item.setContent(POJOHelper.serialize(updated.getEntity()));
349                 }
350 
351                 return item;
352             }).collect(Collectors.toList());
353         } else {
354             batchResponseItems = updated.getPropagationStatuses().stream().
355                     map(status -> {
356                         BatchResponseItem item = new BatchResponseItem();
357 
358                         item.getHeaders().put(RESTHeaders.RESOURCE_KEY, List.of(status.getResource()));
359 
360                         item.setStatus(status.getStatus().getHttpStatus());
361 
362                         if (status.getFailureReason() != null) {
363                             item.getHeaders().put(RESTHeaders.ERROR_INFO, List.of(status.getFailureReason()));
364                         }
365 
366                         if (getPreference() == Preference.RETURN_NO_CONTENT) {
367                             item.getHeaders().put(
368                                     RESTHeaders.PREFERENCE_APPLIED,
369                                     List.of(Preference.RETURN_NO_CONTENT.toString()));
370                         } else {
371                             item.setContent(POJOHelper.serialize(updated.getEntity()));
372                         }
373 
374                         return item;
375                     }).collect(Collectors.toList());
376         }
377 
378         String boundary = "associate_" + SecureRandomUtils.generateRandomUUID().toString();
379         return Response.ok(BatchPayloadGenerator.generate(
380                 batchResponseItems, JAXRSService.DOUBLE_DASH + boundary)).
381                 type(RESTHeaders.multipartMixedWith(boundary)).
382                 build();
383     }
384 }