1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 }