/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.core.provisioning.java.propagation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.MapContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.patch.StringPatchItem;
import org.apache.syncope.common.lib.patch.UserPatch;
import org.apache.syncope.common.lib.to.AttrTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.core.provisioning.api.PropagationByResource;
import org.apache.syncope.common.lib.types.ResourceOperation;
import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
import org.apache.syncope.core.persistence.api.dao.GroupDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
import org.apache.syncope.core.provisioning.api.WorkflowResult;
import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
import org.apache.syncope.core.persistence.api.dao.AnyDAO;
import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
import org.apache.syncope.core.persistence.api.entity.Any;
import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
import org.apache.syncope.core.persistence.api.entity.Realm;
import org.apache.syncope.core.persistence.api.entity.VirSchema;
import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
import org.apache.syncope.core.persistence.api.entity.group.Group;
import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
import org.apache.syncope.core.persistence.api.entity.resource.Provision;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.apache.syncope.core.provisioning.api.MappingManager;
import org.apache.syncope.core.provisioning.api.utils.EntityUtils;
import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.AttributeUtil;
import org.identityconnectors.framework.common.objects.Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
/**
* Manage the data propagation to external resources.
*/
@Transactional(rollbackFor = { Throwable.class })
public class PropagationManagerImpl implements PropagationManager {
protected static final Logger LOG = LoggerFactory.getLogger(PropagationManager.class);
@Autowired
protected VirSchemaDAO virSchemaDAO;
@Autowired
protected AnyObjectDAO anyObjectDAO;
/**
* User DAO.
*/
@Autowired
protected UserDAO userDAO;
/**
* Group DAO.
*/
@Autowired
protected GroupDAO groupDAO;
/**
* Resource DAO.
*/
@Autowired
protected ExternalResourceDAO resourceDAO;
@Autowired
protected EntityFactory entityFactory;
/**
* ConnObjectUtils.
*/
@Autowired
protected ConnObjectUtils connObjectUtils;
@Autowired
protected MappingManager mappingManager;
@Autowired
protected AnyUtilsFactory anyUtilsFactory;
protected Any<?> find(final AnyTypeKind kind, final String key) {
AnyDAO<? extends Any<?>> dao;
switch (kind) {
case ANY_OBJECT:
dao = anyObjectDAO;
break;
case GROUP:
dao = groupDAO;
break;
case USER:
default:
dao = userDAO;
}
return dao.authFind(key);
}
@Override
public List<PropagationTask> getCreateTasks(
final AnyTypeKind kind,
final String key,
final PropagationByResource propByRes,
final Collection<AttrTO> vAttrs,
final Collection<String> noPropResourceKeys) {
return getCreateTasks(find(kind, key), null, null, propByRes, vAttrs, noPropResourceKeys);
}
@Override
public List<PropagationTask> getUserCreateTasks(
final String key,
final String password,
final Boolean enable,
final PropagationByResource propByRes,
final Collection<AttrTO> vAttrs,
final Collection<String> noPropResourceKeys) {
return getCreateTasks(userDAO.authFind(key), password, enable, propByRes, vAttrs, noPropResourceKeys);
}
protected List<PropagationTask> getCreateTasks(
final Any<?> any,
final String password,
final Boolean enable,
final PropagationByResource propByRes,
final Collection<AttrTO> vAttrs,
final Collection<String> noPropResourceKeys) {
if (propByRes == null || propByRes.isEmpty()) {
return Collections.<PropagationTask>emptyList();
}
if (noPropResourceKeys != null) {
propByRes.get(ResourceOperation.CREATE).removeAll(noPropResourceKeys);
}
return createTasks(any, password, true, enable, false, propByRes, vAttrs);
}
@Override
public List<PropagationTask> getUpdateTasks(
final AnyTypeKind kind,
final String key,
final boolean changePwd,
final Boolean enable,
final PropagationByResource propByRes,
final Collection<AttrTO> vAttrs,
final Collection<String> noPropResourceKeys) {
return getUpdateTasks(find(kind, key), null, changePwd, enable, propByRes, vAttrs, noPropResourceKeys);
}
@Override
public List<PropagationTask> getUserUpdateTasks(
final WorkflowResult<Pair<UserPatch, Boolean>> wfResult,
final boolean changePwd,
final Collection<String> noPropResourceKeys) {
return getUpdateTasks(
userDAO.authFind(wfResult.getResult().getKey().getKey()),
wfResult.getResult().getKey().getPassword() == null
? null
: wfResult.getResult().getKey().getPassword().getValue(),
changePwd,
wfResult.getResult().getValue(),
wfResult.getPropByRes(),
wfResult.getResult().getKey().getVirAttrs(),
noPropResourceKeys);
}
@Override
public List<PropagationTask> getUserUpdateTasks(final WorkflowResult<Pair<UserPatch, Boolean>> wfResult) {
UserPatch userPatch = wfResult.getResult().getKey();
// Propagate password update only to requested resources
List<PropagationTask> tasks = new ArrayList<>();
if (userPatch.getPassword() == null) {
// a. no specific password propagation request: generate propagation tasks for any resource associated
tasks = getUserUpdateTasks(wfResult, false, null);
} else {
// b. generate the propagation task list in two phases: first the ones containing password,
// the the rest (with no password)
PropagationByResource origPropByRes = new PropagationByResource();
origPropByRes.merge(wfResult.getPropByRes());
Set<String> pwdResourceNames = new HashSet<>(userPatch.getPassword().getResources());
Collection<String> currentResourceNames = userDAO.findAllResourceKeys(userPatch.getKey());
pwdResourceNames.retainAll(currentResourceNames);
PropagationByResource pwdPropByRes = new PropagationByResource();
pwdPropByRes.addAll(ResourceOperation.UPDATE, pwdResourceNames);
if (!pwdPropByRes.isEmpty()) {
Set<String> toBeExcluded = new HashSet<>(currentResourceNames);
toBeExcluded.addAll(CollectionUtils.collect(userPatch.getResources(),
new Transformer<StringPatchItem, String>() {
@Override
public String transform(final StringPatchItem input) {
return input.getValue();
}
}));
toBeExcluded.removeAll(pwdResourceNames);
tasks.addAll(getUserUpdateTasks(wfResult, true, toBeExcluded));
}
PropagationByResource nonPwdPropByRes = new PropagationByResource();
nonPwdPropByRes.merge(origPropByRes);
nonPwdPropByRes.removeAll(pwdResourceNames);
nonPwdPropByRes.purge();
if (!nonPwdPropByRes.isEmpty()) {
tasks.addAll(getUserUpdateTasks(wfResult, false, pwdResourceNames));
}
}
return tasks;
}
protected List<PropagationTask> getUpdateTasks(
final Any<?> any,
final String password,
final boolean changePwd,
final Boolean enable,
final PropagationByResource propByRes,
final Collection<AttrTO> vAttrs,
final Collection<String> noPropResourceKeys) {
if (noPropResourceKeys != null && propByRes != null) {
propByRes.removeAll(noPropResourceKeys);
}
return createTasks(
any,
password,
changePwd,
enable,
false,
propByRes == null ? new PropagationByResource() : propByRes,
vAttrs);
}
@Override
public List<PropagationTask> getDeleteTasks(
final AnyTypeKind kind,
final String key,
final PropagationByResource propByRes,
final Collection<String> noPropResourceKeys) {
Any<?> any = find(kind, key);
PropagationByResource localPropByRes = new PropagationByResource();
if (propByRes == null || propByRes.isEmpty()) {
localPropByRes.addAll(ResourceOperation.DELETE, any.getResourceKeys());
} else {
localPropByRes.merge(propByRes);
}
if (noPropResourceKeys != null) {
localPropByRes.removeAll(noPropResourceKeys);
}
return getDeleteTasks(any, localPropByRes, noPropResourceKeys);
}
protected List<PropagationTask> getDeleteTasks(
final Any<?> any,
final PropagationByResource propByRes,
final Collection<String> noPropResourceKeys) {
return createTasks(any, null, false, false, true, propByRes, null);
}
/**
* Create propagation tasks.
*
* @param any to be provisioned
* @param password clear text password to be provisioned
* @param changePwd whether password should be included for propagation attributes or not
* @param enable whether user must be enabled or not
* @param deleteOnResource whether any must be deleted anyway from external resource or not
* @param propByRes operation to be performed per resource
* @param vAttrs virtual attributes to be set
* @return list of propagation tasks created
*/
protected List<PropagationTask> createTasks(final Any<?> any,
final String password, final boolean changePwd,
final Boolean enable, final boolean deleteOnResource, final PropagationByResource propByRes,
final Collection<AttrTO> vAttrs) {
LOG.debug("Provisioning {}:\n{}", any, propByRes);
// Avoid duplicates - see javadoc
propByRes.purge();
LOG.debug("After purge {}:\n{}", any, propByRes);
// Virtual attributes
Set<String> virtualResources = new HashSet<>();
virtualResources.addAll(propByRes.get(ResourceOperation.CREATE));
virtualResources.addAll(propByRes.get(ResourceOperation.UPDATE));
if (any instanceof User) {
virtualResources.addAll(CollectionUtils.collect(
userDAO.findAllResources((User) any), EntityUtils.keyTransformer()));
} else if (any instanceof AnyObject) {
virtualResources.addAll(CollectionUtils.collect(
anyObjectDAO.findAllResources((AnyObject) any), EntityUtils.keyTransformer()));
} else {
virtualResources.addAll(((Group) any).getResourceKeys());
}
Map<String, Set<Attribute>> vAttrMap = new HashMap<>();
for (AttrTO vAttr : CollectionUtils.emptyIfNull(vAttrs)) {
VirSchema schema = virSchemaDAO.find(vAttr.getSchema());
if (schema == null) {
LOG.warn("Ignoring invalid {} {}", VirSchema.class.getSimpleName(), vAttr.getSchema());
} else if (schema.isReadonly()) {
LOG.warn("Ignoring read-only {} {}", VirSchema.class.getSimpleName(), vAttr.getSchema());
} else if (anyUtilsFactory.getInstance(any).getAllowedSchemas(any, VirSchema.class).contains(schema)
&& virtualResources.contains(schema.getProvision().getResource().getKey())) {
Set<Attribute> values = vAttrMap.get(schema.getProvision().getResource().getKey());
if (values == null) {
values = new HashSet<>();
vAttrMap.put(schema.getProvision().getResource().getKey(), values);
}
values.add(AttributeBuilder.build(schema.getExtAttrName(), vAttr.getValues()));
propByRes.add(ResourceOperation.UPDATE, schema.getProvision().getResource().getKey());
} else {
LOG.warn("{} not owned by or {} not allowed for {}",
schema.getProvision().getResource(), schema, any);
}
}
LOG.debug("With virtual attributes {}:\n{}\n{}", any, propByRes, vAttrMap);
List<PropagationTask> tasks = new ArrayList<>();
for (Map.Entry<String, ResourceOperation> entry : propByRes.asMap().entrySet()) {
ExternalResource resource = resourceDAO.find(entry.getKey());
Provision provision = resource == null ? null : resource.getProvision(any.getType());
List<MappingItem> mappingItems = provision == null
? Collections.<MappingItem>emptyList()
: MappingUtils.getPropagationMappingItems(provision);
if (resource == null) {
LOG.error("Invalid resource name specified: {}, ignoring...", entry.getKey());
} else if (provision == null) {
LOG.error("No provision specified on resource {} for type {}, ignoring...",
resource, any.getType());
} else if (mappingItems.isEmpty()) {
LOG.warn("Requesting propagation for {} but no propagation mapping provided for {}",
any.getType(), resource);
} else {
PropagationTask task = entityFactory.newEntity(PropagationTask.class);
task.setResource(resource);
task.setObjectClassName(provision.getObjectClass().getObjectClassValue());
task.setAnyTypeKind(any.getType().getKind());
task.setAnyType(any.getType().getKey());
if (!deleteOnResource) {
task.setEntityKey(any.getKey());
}
task.setOperation(entry.getValue());
task.setOldConnObjectKey(propByRes.getOldConnObjectKey(resource.getKey()));
Pair<String, Set<Attribute>> preparedAttrs =
mappingManager.prepareAttrs(any, password, changePwd, enable, provision);
task.setConnObjectKey(preparedAttrs.getKey());
// Check if any of mandatory attributes (in the mapping) is missing or not received any value:
// if so, add special attributes that will be evaluated by PropagationTaskExecutor
List<String> mandatoryMissing = new ArrayList<>();
List<String> mandatoryNullOrEmpty = new ArrayList<>();
for (MappingItem item : mappingItems) {
if (!item.isConnObjectKey()
&& JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any)) {
Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.getValue());
if (attr == null) {
mandatoryMissing.add(item.getExtAttrName());
} else if (attr.getValue() == null || attr.getValue().isEmpty()) {
mandatoryNullOrEmpty.add(item.getExtAttrName());
}
}
}
if (!mandatoryMissing.isEmpty()) {
preparedAttrs.getValue().add(AttributeBuilder.build(
PropagationTaskExecutor.MANDATORY_MISSING_ATTR_NAME, mandatoryMissing));
}
if (!mandatoryNullOrEmpty.isEmpty()) {
preparedAttrs.getValue().add(AttributeBuilder.build(
PropagationTaskExecutor.MANDATORY_NULL_OR_EMPTY_ATTR_NAME, mandatoryNullOrEmpty));
}
if (vAttrMap.containsKey(resource.getKey())) {
preparedAttrs.getValue().addAll(vAttrMap.get(resource.getKey()));
}
task.setAttributes(preparedAttrs.getValue());
tasks.add(task);
LOG.debug("PropagationTask created: {}", task);
}
}
return tasks;
}
@Override
public List<PropagationTask> createTasks(
final Realm realm,
final PropagationByResource propByRes,
final Collection<String> noPropResourceKeys) {
if (noPropResourceKeys != null) {
propByRes.removeAll(noPropResourceKeys);
}
LOG.debug("Provisioning {}:\n{}", realm, propByRes);
// Avoid duplicates - see javadoc
propByRes.purge();
LOG.debug("After purge {}:\n{}", realm, propByRes);
List<PropagationTask> tasks = new ArrayList<>();
for (Map.Entry<String, ResourceOperation> entry : propByRes.asMap().entrySet()) {
ExternalResource resource = resourceDAO.find(entry.getKey());
OrgUnit orgUnit = resource == null ? null : resource.getOrgUnit();
if (resource == null) {
LOG.error("Invalid resource name specified: {}, ignoring...", entry.getKey());
} else if (orgUnit == null) {
LOG.error("No orgUnit specified on resource {}, ignoring...", resource);
} else if (StringUtils.isBlank(orgUnit.getConnObjectLink())) {
LOG.warn("Requesting propagation for {} but no ConnObjectLink provided for {}",
realm.getFullPath(), resource);
} else {
PropagationTask task = entityFactory.newEntity(PropagationTask.class);
task.setResource(resource);
task.setObjectClassName(orgUnit.getObjectClass().getObjectClassValue());
task.setEntityKey(realm.getKey());
task.setOperation(entry.getValue());
Set<Attribute> preparedAttrs = new HashSet<>();
preparedAttrs.add(AttributeBuilder.build(orgUnit.getExtAttrName(), realm.getName()));
JexlContext jexlContext = new MapContext();
JexlUtils.addFieldsToContext(realm, jexlContext);
String evalConnObjectLink = JexlUtils.evaluate(orgUnit.getConnObjectLink(), jexlContext);
if (StringUtils.isBlank(evalConnObjectLink)) {
// add connObjectKey as __NAME__ attribute ...
LOG.debug("Add connObjectKey [{}] as __NAME__", realm.getName());
preparedAttrs.add(new Name(realm.getName()));
} else {
LOG.debug("Add connObjectLink [{}] as __NAME__", evalConnObjectLink);
preparedAttrs.add(new Name(evalConnObjectLink));
// connObjectKey not propagated: it will be used to set the value for __UID__ attribute
LOG.debug("connObjectKey will be used just as __UID__ attribute");
}
task.setConnObjectKey(realm.getName());
task.setAttributes(preparedAttrs);
tasks.add(task);
LOG.debug("PropagationTask created: {}", task);
}
}
return tasks;
}
}