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.logic;
20  
21  import java.lang.reflect.Method;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Optional;
25  import java.util.Set;
26  import java.util.stream.Collectors;
27  import java.util.stream.Stream;
28  import org.apache.commons.lang3.ArrayUtils;
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.commons.lang3.tuple.Pair;
31  import org.apache.commons.lang3.tuple.Triple;
32  import org.apache.syncope.common.lib.SyncopeClientException;
33  import org.apache.syncope.common.lib.SyncopeConstants;
34  import org.apache.syncope.common.lib.to.ConnObject;
35  import org.apache.syncope.common.lib.to.Item;
36  import org.apache.syncope.common.lib.to.Provision;
37  import org.apache.syncope.common.lib.to.ResourceTO;
38  import org.apache.syncope.common.lib.types.ClientExceptionType;
39  import org.apache.syncope.common.lib.types.IdMEntitlement;
40  import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
41  import org.apache.syncope.core.persistence.api.dao.ConnInstanceDAO;
42  import org.apache.syncope.core.persistence.api.dao.DuplicateException;
43  import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
44  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
45  import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
46  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
47  import org.apache.syncope.core.persistence.api.entity.Any;
48  import org.apache.syncope.core.persistence.api.entity.AnyType;
49  import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
50  import org.apache.syncope.core.persistence.api.entity.ConnInstance;
51  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
52  import org.apache.syncope.core.persistence.api.entity.VirSchema;
53  import org.apache.syncope.core.provisioning.api.Connector;
54  import org.apache.syncope.core.provisioning.api.ConnectorManager;
55  import org.apache.syncope.core.provisioning.api.MappingManager;
56  import org.apache.syncope.core.provisioning.api.VirAttrHandler;
57  import org.apache.syncope.core.provisioning.api.data.ConnInstanceDataBinder;
58  import org.apache.syncope.core.provisioning.api.data.ResourceDataBinder;
59  import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
60  import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
61  import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
62  import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
63  import org.apache.syncope.core.spring.security.AuthContextUtils;
64  import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
65  import org.identityconnectors.framework.common.objects.ConnectorObject;
66  import org.identityconnectors.framework.common.objects.ObjectClass;
67  import org.identityconnectors.framework.common.objects.OperationOptions;
68  import org.identityconnectors.framework.common.objects.SearchResult;
69  import org.identityconnectors.framework.common.objects.filter.Filter;
70  import org.identityconnectors.framework.spi.SearchResultsHandler;
71  import org.springframework.security.access.prepost.PreAuthorize;
72  import org.springframework.transaction.annotation.Transactional;
73  
74  public class ResourceLogic extends AbstractTransactionalLogic<ResourceTO> {
75  
76      protected final ExternalResourceDAO resourceDAO;
77  
78      protected final AnyTypeDAO anyTypeDAO;
79  
80      protected final ConnInstanceDAO connInstanceDAO;
81  
82      protected final VirSchemaDAO virSchemaDAO;
83  
84      protected final VirAttrHandler virAttrHandler;
85  
86      protected final ResourceDataBinder binder;
87  
88      protected final ConnInstanceDataBinder connInstanceDataBinder;
89  
90      protected final OutboundMatcher outboundMatcher;
91  
92      protected final MappingManager mappingManager;
93  
94      protected final ConnectorManager connectorManager;
95  
96      protected final AnyUtilsFactory anyUtilsFactory;
97  
98      public ResourceLogic(
99              final ExternalResourceDAO resourceDAO,
100             final AnyTypeDAO anyTypeDAO,
101             final ConnInstanceDAO connInstanceDAO,
102             final VirSchemaDAO virSchemaDAO,
103             final VirAttrHandler virAttrHandler,
104             final ResourceDataBinder binder,
105             final ConnInstanceDataBinder connInstanceDataBinder,
106             final OutboundMatcher outboundMatcher,
107             final MappingManager mappingManager,
108             final ConnectorManager connectorManager,
109             final AnyUtilsFactory anyUtilsFactory) {
110 
111         this.resourceDAO = resourceDAO;
112         this.anyTypeDAO = anyTypeDAO;
113         this.connInstanceDAO = connInstanceDAO;
114         this.virSchemaDAO = virSchemaDAO;
115         this.virAttrHandler = virAttrHandler;
116         this.binder = binder;
117         this.connInstanceDataBinder = connInstanceDataBinder;
118         this.outboundMatcher = outboundMatcher;
119         this.mappingManager = mappingManager;
120         this.connectorManager = connectorManager;
121         this.anyUtilsFactory = anyUtilsFactory;
122     }
123 
124     protected void securityChecks(final Set<String> effectiveRealms, final String realm, final String key) {
125         boolean authorized = effectiveRealms.stream().anyMatch(realm::startsWith);
126         if (!authorized) {
127             throw new DelegatedAdministrationException(realm, ExternalResource.class.getSimpleName(), key);
128         }
129     }
130 
131     protected ExternalResource doSave(final ExternalResource resource) {
132         ExternalResource merged = resourceDAO.save(resource);
133         try {
134             connectorManager.registerConnector(merged);
135         } catch (NotFoundException e) {
136             LOG.error("While registering connector for resource", e);
137         }
138         return merged;
139     }
140 
141     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_CREATE + "')")
142     public ResourceTO create(final ResourceTO resourceTO) {
143         if (StringUtils.isBlank(resourceTO.getKey())) {
144             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.RequiredValuesMissing);
145             sce.getElements().add("Resource key");
146             throw sce;
147         }
148 
149         ConnInstance connInstance = connInstanceDAO.authFind(resourceTO.getConnector());
150         if (connInstance == null) {
151             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidExternalResource);
152             sce.getElements().add("Connector " + resourceTO.getConnector());
153             throw sce;
154         }
155 
156         Set<String> effectiveRealms = RealmUtils.getEffective(
157                 AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_CREATE),
158                 connInstance.getAdminRealm().getFullPath());
159         securityChecks(effectiveRealms, connInstance.getAdminRealm().getFullPath(), null);
160 
161         if (resourceDAO.authFind(resourceTO.getKey()) != null) {
162             throw new DuplicateException(resourceTO.getKey());
163         }
164 
165         return binder.getResourceTO(doSave(binder.create(resourceTO)));
166     }
167 
168     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_UPDATE + "')")
169     public ResourceTO update(final ResourceTO resourceTO) {
170         ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(resourceTO.getKey())).
171                 orElseThrow(() -> new NotFoundException("Resource '" + resourceTO.getKey() + '\''));
172 
173         Set<String> effectiveRealms = RealmUtils.getEffective(
174                 AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_UPDATE),
175                 resource.getConnector().getAdminRealm().getFullPath());
176         securityChecks(effectiveRealms, resource.getConnector().getAdminRealm().getFullPath(), resource.getKey());
177 
178         return binder.getResourceTO(doSave(binder.update(resource, resourceTO)));
179     }
180 
181     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_UPDATE + "')")
182     public void setLatestSyncToken(final String key, final String anyTypeKey) {
183         ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)).
184                 orElseThrow(() -> new NotFoundException("Resource '" + key + '\''));
185 
186         Connector connector;
187         try {
188             connector = connectorManager.getConnector(resource);
189         } catch (Exception e) {
190             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidConnInstance);
191             sce.getElements().add(e.getMessage());
192             throw sce;
193         }
194 
195         if (SyncopeConstants.REALM_ANYTYPE.equals(anyTypeKey)) {
196             if (resource.getOrgUnit() == null) {
197                 throw new NotFoundException("Realm provision not enabled for Resource '" + key + '\'');
198             }
199 
200             resource.getOrgUnit().setSyncToken(ConnObjectUtils.toString(
201                     connector.getLatestSyncToken(new ObjectClass(resource.getOrgUnit().getObjectClass()))));
202         } else {
203             AnyType anyType = Optional.ofNullable(anyTypeDAO.find(anyTypeKey)).
204                     orElseThrow(() -> new NotFoundException("AnyType '" + anyTypeKey + '\''));
205             Provision provision = resource.getProvisionByAnyType(anyType.getKey()).
206                     orElseThrow(() -> new NotFoundException(
207                     "Provision for AnyType '" + anyTypeKey + "' in Resource '" + key + '\''));
208 
209             provision.setSyncToken(ConnObjectUtils.toString(
210                     connector.getLatestSyncToken(new ObjectClass(provision.getObjectClass()))));
211         }
212 
213         Set<String> effectiveRealms = RealmUtils.getEffective(
214                 AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_UPDATE),
215                 resource.getConnector().getAdminRealm().getFullPath());
216         securityChecks(effectiveRealms, resource.getConnector().getAdminRealm().getFullPath(), resource.getKey());
217 
218         doSave(resource);
219     }
220 
221     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_UPDATE + "')")
222     public void removeSyncToken(final String key, final String anyTypeKey) {
223         ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)).
224                 orElseThrow(() -> new NotFoundException("Resource '" + key + '\''));
225         if (SyncopeConstants.REALM_ANYTYPE.equals(anyTypeKey)) {
226             if (resource.getOrgUnit() == null) {
227                 throw new NotFoundException("Realm provision not enabled for Resource '" + key + '\'');
228             }
229 
230             resource.getOrgUnit().setSyncToken(null);
231         } else {
232             AnyType anyType = Optional.ofNullable(anyTypeDAO.find(anyTypeKey)).
233                     orElseThrow(() -> new NotFoundException("AnyType '" + anyTypeKey + '\''));
234             Provision provision = resource.getProvisionByAnyType(anyType.getKey()).
235                     orElseThrow(() -> new NotFoundException(
236                     "Provision for AnyType '" + anyTypeKey + "' in Resource '" + key + '\''));
237 
238             provision.setSyncToken(null);
239         }
240 
241         Set<String> effectiveRealms = RealmUtils.getEffective(
242                 AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_UPDATE),
243                 resource.getConnector().getAdminRealm().getFullPath());
244         securityChecks(effectiveRealms, resource.getConnector().getAdminRealm().getFullPath(), resource.getKey());
245 
246         doSave(resource);
247     }
248 
249     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_DELETE + "')")
250     public ResourceTO delete(final String key) {
251         ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)).
252                 orElseThrow(() -> new NotFoundException("Resource '" + key + '\''));
253 
254         Set<String> effectiveRealms = RealmUtils.getEffective(
255                 AuthContextUtils.getAuthorizations().get(IdMEntitlement.RESOURCE_DELETE),
256                 resource.getConnector().getAdminRealm().getFullPath());
257         securityChecks(effectiveRealms, resource.getConnector().getAdminRealm().getFullPath(), resource.getKey());
258 
259         connectorManager.unregisterConnector(resource);
260 
261         ResourceTO deleted = binder.getResourceTO(resource);
262         resourceDAO.delete(key);
263         return deleted;
264     }
265 
266     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_READ + "')")
267     @Transactional(readOnly = true)
268     public ResourceTO read(final String key) {
269         ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(key)).
270                 orElseThrow(() -> new NotFoundException("Resource '" + key + '\''));
271 
272         return binder.getResourceTO(resource);
273     }
274 
275     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_LIST + "')")
276     @Transactional(readOnly = true)
277     public List<ResourceTO> list() {
278         return resourceDAO.findAll().stream().map(binder::getResourceTO).collect(Collectors.toList());
279     }
280 
281     protected Triple<AnyType, ExternalResource, Provision> getProvision(
282             final String anyTypeKey, final String resourceKey) {
283 
284         AnyType anyType = Optional.ofNullable(anyTypeDAO.find(anyTypeKey)).
285                 orElseThrow(() -> new NotFoundException("AnyType '" + anyTypeKey + '\''));
286 
287         ExternalResource resource = Optional.ofNullable(resourceDAO.authFind(resourceKey)).
288                 orElseThrow(() -> new NotFoundException("Resource '" + resourceKey + '\''));
289         Provision provision = resource.getProvisionByAnyType(anyType.getKey()).
290                 orElseThrow(() -> new NotFoundException(
291                 "Provision for " + anyType + " on Resource '" + resourceKey + "'"));
292         if (provision.getMapping() == null) {
293             throw new NotFoundException("Mapping for " + anyType + " on Resource '" + resourceKey + "'");
294         }
295 
296         return Triple.of(anyType, resource, provision);
297     }
298 
299     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
300     @Transactional(readOnly = true)
301     public String getConnObjectKeyValue(
302             final String key,
303             final String anyTypeKey,
304             final String anyKey) {
305 
306         Triple<AnyType, ExternalResource, Provision> triple = getProvision(anyTypeKey, key);
307 
308         // 1. find any
309         Any<?> any = Optional.ofNullable(anyUtilsFactory.getInstance(triple.getLeft().getKind()).
310                 dao().authFind(anyKey)).
311                 orElseThrow(() -> new NotFoundException(triple.getLeft() + " " + anyKey));
312 
313         // 2.get ConnObjectKey value
314         return mappingManager.getConnObjectKeyValue(any, triple.getMiddle(), triple.getRight()).
315                 orElseThrow(() -> new NotFoundException(
316                 "No ConnObjectKey value found for " + anyTypeKey + " " + anyKey + " on resource '" + key + "'"));
317     }
318 
319     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
320     @Transactional(readOnly = true)
321     public ConnObject readConnObjectByAnyKey(
322             final String key,
323             final String anyTypeKey,
324             final String anyKey) {
325 
326         Triple<AnyType, ExternalResource, Provision> triple = getProvision(anyTypeKey, key);
327 
328         // 1. find any
329         Any<?> any = Optional.ofNullable(anyUtilsFactory.getInstance(triple.getLeft().getKind()).
330                 dao().authFind(anyKey)).
331                 orElseThrow(() -> new NotFoundException(triple.getLeft() + " " + anyKey));
332 
333         // 2. find on resource
334         List<ConnectorObject> connObjs = outboundMatcher.match(
335                 connectorManager.getConnector(triple.getMiddle()),
336                 any,
337                 triple.getMiddle(),
338                 triple.getRight(),
339                 Optional.empty());
340         if (connObjs.isEmpty()) {
341             throw new NotFoundException(
342                     "Object " + any + " with class " + triple.getRight().getObjectClass()
343                     + " not found on resource " + triple.getMiddle().getKey());
344         }
345 
346         if (connObjs.size() > 1) {
347             LOG.warn("Expected single match, found {}", connObjs);
348         } else {
349             virAttrHandler.setValues(any, connObjs.get(0));
350         }
351 
352         return ConnObjectUtils.getConnObjectTO(
353                 outboundMatcher.getFIQL(connObjs.get(0), triple.getMiddle(), triple.getRight()),
354                 connObjs.get(0).getAttributes());
355     }
356 
357     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_GET_CONNOBJECT + "')")
358     @Transactional(readOnly = true)
359     public ConnObject readConnObjectByConnObjectKeyValue(
360             final String key,
361             final String anyTypeKey,
362             final String connObjectKeyValue) {
363 
364         Triple<AnyType, ExternalResource, Provision> triple = getProvision(anyTypeKey, key);
365 
366         Item connObjectKeyItem = MappingUtils.getConnObjectKeyItem(triple.getRight()).
367                 orElseThrow(() -> new NotFoundException(
368                 "ConnObjectKey mapping for " + triple.getLeft().getKey()
369                 + " on resource '" + triple.getMiddle().getKey() + "'"));
370 
371         return outboundMatcher.matchByConnObjectKeyValue(
372                 connectorManager.getConnector(triple.getMiddle()),
373                 connObjectKeyItem,
374                 connObjectKeyValue,
375                 triple.getMiddle(),
376                 triple.getRight(),
377                 Optional.empty(),
378                 Optional.empty()).
379                 map(connectorObject -> ConnObjectUtils.getConnObjectTO(
380                 outboundMatcher.getFIQL(connectorObject, triple.getMiddle(), triple.getRight()),
381                 connectorObject.getAttributes())).
382                 orElseThrow(() -> new NotFoundException(
383                 "Object " + connObjectKeyValue + " with class " + triple.getRight().getObjectClass()
384                 + " not found on resource " + triple.getMiddle().getKey()));
385     }
386 
387     @PreAuthorize("hasRole('" + IdMEntitlement.RESOURCE_LIST_CONNOBJECT + "')")
388     @Transactional(readOnly = true)
389     public Pair<SearchResult, List<ConnObject>> searchConnObjects(
390             final Filter filter,
391             final Set<String> moreAttrsToGet,
392             final String key,
393             final String anyTypeKey,
394             final int size,
395             final String pagedResultsCookie,
396             final List<OrderByClause> orderBy) {
397 
398         ExternalResource resource;
399         Provision provision;
400         ObjectClass objectClass;
401         OperationOptions options;
402         if (SyncopeConstants.REALM_ANYTYPE.equals(anyTypeKey)) {
403             resource = resourceDAO.find(key);
404             if (resource == null) {
405                 throw new NotFoundException("Resource '" + key + '\'');
406             }
407             if (resource.getOrgUnit() == null) {
408                 throw new NotFoundException("Realm provisioning for resource '" + key + '\'');
409             }
410 
411             provision = null;
412             objectClass = new ObjectClass(resource.getOrgUnit().getObjectClass());
413             options = MappingUtils.buildOperationOptions(
414                     resource.getOrgUnit().getItems().stream(), moreAttrsToGet.toArray(String[]::new));
415         } else {
416             Triple<AnyType, ExternalResource, Provision> triple = getProvision(anyTypeKey, key);
417 
418             provision = triple.getRight();
419             resource = triple.getMiddle();
420             objectClass = new ObjectClass(provision.getObjectClass());
421 
422             Stream<Item> mapItems = Stream.concat(
423                     provision.getMapping().getItems().stream(),
424                     virSchemaDAO.find(resource.getKey(), triple.getLeft().getKey()).
425                             stream().map(VirSchema::asLinkingMappingItem));
426             options = MappingUtils.buildOperationOptions(mapItems, moreAttrsToGet.toArray(String[]::new));
427         }
428 
429         List<ConnObject> connObjects = new ArrayList<>();
430         SearchResult searchResult = connectorManager.getConnector(resource).
431                 search(objectClass, filter, new SearchResultsHandler() {
432 
433                     private int count;
434 
435                     @Override
436                     public boolean handle(final ConnectorObject connectorObject) {
437                         connObjects.add(ConnObjectUtils.getConnObjectTO(
438                                 provision == null
439                                         ? null : outboundMatcher.getFIQL(connectorObject, resource, provision),
440                                 connectorObject.getAttributes()));
441                         // safety protection against uncontrolled result size
442                         count++;
443                         return count < size;
444                     }
445 
446                     @Override
447                     public void handleResult(final SearchResult sr) {
448                         // do nothing
449                     }
450                 }, size, pagedResultsCookie, orderBy, options);
451 
452         return Pair.of(searchResult, connObjects);
453     }
454 
455     @PreAuthorize("hasRole('" + IdMEntitlement.CONNECTOR_READ + "')")
456     @Transactional(readOnly = true)
457     public void check(final ResourceTO resourceTO) {
458         ConnInstance connInstance = Optional.ofNullable(connInstanceDAO.find(resourceTO.getConnector())).
459                 orElseThrow(() -> new NotFoundException("Connector '" + resourceTO.getConnector() + '\''));
460 
461         connectorManager.createConnector(
462                 connectorManager.buildConnInstanceOverride(
463                         connInstanceDataBinder.getConnInstanceTO(connInstance),
464                         resourceTO.getConfOverride(),
465                         resourceTO.isOverrideCapabilities()
466                         ? Optional.of(resourceTO.getCapabilitiesOverride()) : Optional.empty())).
467                 test();
468     }
469 
470     @Override
471     protected ResourceTO resolveReference(final Method method, final Object... args)
472             throws UnresolvedReferenceException {
473 
474         String key = null;
475 
476         if (ArrayUtils.isNotEmpty(args)) {
477             for (int i = 0; key == null && i < args.length; i++) {
478                 if (args[i] instanceof String) {
479                     key = (String) args[i];
480                 } else if (args[i] instanceof ResourceTO) {
481                     key = ((ResourceTO) args[i]).getKey();
482                 }
483             }
484         }
485 
486         if (key != null) {
487             try {
488                 return binder.getResourceTO(resourceDAO.find(key));
489             } catch (Throwable ignore) {
490                 LOG.debug("Unresolved reference", ignore);
491                 throw new UnresolvedReferenceException(ignore);
492             }
493         }
494 
495         throw new UnresolvedReferenceException();
496     }
497 }