http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTaskJob.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTaskJob.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTaskJob.java new file mode 100644 index 0000000..2eddca1 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTaskJob.java @@ -0,0 +1,183 @@ +/* + * 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.job; + +import java.util.Date; +import org.apache.syncope.common.lib.types.AuditElements; +import org.apache.syncope.common.lib.types.AuditElements.Result; +import org.apache.syncope.core.persistence.api.dao.TaskDAO; +import org.apache.syncope.core.persistence.api.dao.TaskExecDAO; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.task.Task; +import org.apache.syncope.core.persistence.api.entity.task.TaskExec; +import org.apache.syncope.core.provisioning.api.job.TaskJob; +import org.apache.syncope.core.misc.AuditManager; +import org.apache.syncope.core.misc.ExceptionUtil; +import org.apache.syncope.core.provisioning.api.notification.NotificationManager; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Abstract job implementation that delegates to concrete implementation the actual job execution and provides some + * base features. + * <strong>Extending this class will not provide support transaction management.</strong><br/> + * Extend <tt>AbstractTransactionalTaskJob</tt> for this purpose. + * + * @see AbstractTransactionalTaskJob + */ +@DisallowConcurrentExecution +public abstract class AbstractTaskJob implements TaskJob { + + public static final String DRY_RUN_JOBDETAIL_KEY = "dryRun"; + + /** + * Task execution status. + */ + public enum Status { + + SUCCESS, + FAILURE + + } + + /** + * Logger. + */ + protected static final Logger LOG = LoggerFactory.getLogger(AbstractTaskJob.class); + + /** + * Task DAO. + */ + @Autowired + protected TaskDAO taskDAO; + + /** + * Task execution DAO. + */ + @Autowired + private TaskExecDAO taskExecDAO; + + /** + * Notification manager. + */ + @Autowired + private NotificationManager notificationManager; + + /** + * Audit manager. + */ + @Autowired + private AuditManager auditManager; + + @Autowired + private EntityFactory entityFactory; + + /** + * Id, set by the caller, for identifying the task to be executed. + */ + protected Long taskId; + + /** + * The actual task to be executed. + */ + protected Task task; + + /** + * Task id setter. + * + * @param taskId to be set + */ + @Override + public void setTaskId(final Long taskId) { + this.taskId = taskId; + } + + @Override + public void execute(final JobExecutionContext context) throws JobExecutionException { + task = taskDAO.find(taskId); + if (task == null) { + throw new JobExecutionException("Task " + taskId + " not found"); + } + + TaskExec execution = entityFactory.newEntity(TaskExec.class); + execution.setStartDate(new Date()); + execution.setTask(task); + + Result result; + + try { + execution.setMessage(doExecute(context.getMergedJobDataMap().getBoolean(DRY_RUN_JOBDETAIL_KEY))); + execution.setStatus(Status.SUCCESS.name()); + result = Result.SUCCESS; + } catch (JobExecutionException e) { + LOG.error("While executing task " + taskId, e); + result = Result.FAILURE; + + execution.setMessage(ExceptionUtil.getFullStackTrace(e)); + execution.setStatus(Status.FAILURE.name()); + } + execution.setEndDate(new Date()); + + if (hasToBeRegistered(execution)) { + taskExecDAO.saveAndAdd(taskId, execution); + } + task = taskDAO.save(task); + + notificationManager.createTasks( + AuditElements.EventCategoryType.TASK, + this.getClass().getSimpleName(), + null, + this.getClass().getSimpleName(), // searching for before object is too much expensive ... + result, + task, + execution); + + auditManager.audit( + AuditElements.EventCategoryType.TASK, + task.getClass().getSimpleName(), + null, + null, // searching for before object is too much expensive ... + result, + task, + (Object[]) null); + } + + /** + * The actual execution, delegated to child classes. + * + * @param dryRun whether to actually touch the data + * @return the task execution status to be set + * @throws JobExecutionException if anything goes wrong + */ + protected abstract String doExecute(boolean dryRun) throws JobExecutionException; + + /** + * Template method to determine whether this job's task execution has to be persisted or not. + * + * @param execution task execution + * @return wether to persist or not + */ + protected boolean hasToBeRegistered(final TaskExec execution) { + return false; + } +}
http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTransactionalTaskJob.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTransactionalTaskJob.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTransactionalTaskJob.java new file mode 100644 index 0000000..b90c142 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractTransactionalTaskJob.java @@ -0,0 +1,35 @@ +/* + * 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.job; + +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.transaction.annotation.Transactional; + +/** + * Abstract job implementation for transactional execution. + */ +public abstract class AbstractTransactionalTaskJob extends AbstractTaskJob { + + @Transactional + @Override + public void execute(final JobExecutionContext context) throws JobExecutionException { + super.execute(context); + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SampleJob.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SampleJob.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SampleJob.java new file mode 100644 index 0000000..e031905 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SampleJob.java @@ -0,0 +1,52 @@ +/* + * 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.job; + +import org.apache.syncope.core.persistence.api.entity.task.SchedTask; +import org.apache.syncope.core.persistence.api.entity.task.TaskExec; +import org.quartz.JobExecutionException; + +/** + * Sample implementation for execution a scheduled task. + * + * @see SchedTask + */ +public class SampleJob extends AbstractTaskJob { + + @Override + protected String doExecute(final boolean dryRun) throws JobExecutionException { + if (!(task instanceof SchedTask)) { + throw new JobExecutionException("Task " + taskId + " isn't a SchedTask"); + } + final SchedTask schedTask = (SchedTask) this.task; + + LOG.info("SampleJob {}running [SchedTask {}]", (dryRun + ? "dry " + : ""), schedTask.getKey()); + + return (dryRun + ? "DRY " + : "") + "RUNNING"; + } + + @Override + protected boolean hasToBeRegistered(final TaskExec execution) { + return true; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SpringBeanJobFactory.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SpringBeanJobFactory.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SpringBeanJobFactory.java new file mode 100644 index 0000000..e54739b --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/SpringBeanJobFactory.java @@ -0,0 +1,97 @@ +/* + * 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.job; + +import org.apache.syncope.core.provisioning.api.job.JobNamer; +import org.apache.syncope.core.provisioning.api.job.JobInstanceLoader; +import org.quartz.SchedulerContext; +import org.quartz.spi.TriggerFiredBundle; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +public class SpringBeanJobFactory extends org.springframework.scheduling.quartz.SpringBeanJobFactory { + + private String[] ignoredUnknownProperties; + + private SchedulerContext schedulerContext; + + @Override + public void setIgnoredUnknownProperties(final String[] ignoredUnknownProperties) { + String[] defensiveCopy = ignoredUnknownProperties.clone(); + super.setIgnoredUnknownProperties(defensiveCopy); + this.ignoredUnknownProperties = defensiveCopy; + } + + @Override + public void setSchedulerContext(final SchedulerContext schedulerContext) { + super.setSchedulerContext(schedulerContext); + this.schedulerContext = schedulerContext; + } + + /** + * An implementation of SpringBeanJobFactory that retrieves the bean from the Spring context so that autowiring and + * transactions work. + * + * {@inheritDoc} + */ + @Override + protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { + final ApplicationContext ctx = ((ConfigurableApplicationContext) schedulerContext.get("applicationContext")); + + // Try to re-create job bean from underlying task (useful for managing failover scenarios) + if (!ctx.containsBean(bundle.getJobDetail().getKey().getName())) { + Long taskId = JobNamer.getTaskIdFromJobName(bundle.getJobDetail().getKey().getName()); + if (taskId != null) { + JobInstanceLoader jobInstanceLoader = ctx.getBean(JobInstanceLoader.class); + jobInstanceLoader.registerTaskJob(taskId); + } + + Long reportId = JobNamer.getReportIdFromJobName(bundle.getJobDetail().getKey().getName()); + if (reportId != null) { + JobInstanceLoader jobInstanceLoader = ctx.getBean(JobInstanceLoader.class); + jobInstanceLoader.registerReportJob(reportId); + } + } + + final Object job = ctx.getBean(bundle.getJobDetail().getKey().getName()); + final BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(job); + if (isEligibleForPropertyPopulation(wrapper.getWrappedInstance())) { + final MutablePropertyValues pvs = new MutablePropertyValues(); + if (this.schedulerContext != null) { + pvs.addPropertyValues(this.schedulerContext); + } + pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap()); + pvs.addPropertyValues(bundle.getTrigger().getJobDataMap()); + if (this.ignoredUnknownProperties == null) { + wrapper.setPropertyValues(pvs, true); + } else { + for (String propName : this.ignoredUnknownProperties) { + if (pvs.contains(propName) && !wrapper.isWritableProperty(propName)) { + pvs.removePropertyValue(propName); + } + } + wrapper.setPropertyValues(pvs); + } + } + return job; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/NotificationManagerImpl.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/NotificationManagerImpl.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/NotificationManagerImpl.java new file mode 100644 index 0000000..67d5a24 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/NotificationManagerImpl.java @@ -0,0 +1,416 @@ +/* + * 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.notification; + +import org.apache.syncope.core.provisioning.api.notification.NotificationManager; +import java.io.StringWriter; +import java.util.ArrayList; +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.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.to.RoleTO; +import org.apache.syncope.common.lib.to.UserTO; +import org.apache.syncope.common.lib.types.AttributableType; +import org.apache.syncope.common.lib.types.AuditElements; +import org.apache.syncope.common.lib.types.AuditElements.Result; +import org.apache.syncope.common.lib.types.AuditLoggerName; +import org.apache.syncope.common.lib.types.IntMappingType; +import org.apache.syncope.common.lib.types.SubjectType; +import org.apache.syncope.core.persistence.api.RoleEntitlementUtil; +import org.apache.syncope.core.persistence.api.dao.ConfDAO; +import org.apache.syncope.core.persistence.api.dao.EntitlementDAO; +import org.apache.syncope.core.persistence.api.dao.NotificationDAO; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; +import org.apache.syncope.core.persistence.api.dao.SubjectSearchDAO; +import org.apache.syncope.core.persistence.api.dao.TaskDAO; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; +import org.apache.syncope.core.persistence.api.entity.Attributable; +import org.apache.syncope.core.persistence.api.entity.AttributableUtilFactory; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.Notification; +import org.apache.syncope.core.persistence.api.entity.PlainAttr; +import org.apache.syncope.core.persistence.api.entity.Subject; +import org.apache.syncope.core.persistence.api.entity.role.Role; +import org.apache.syncope.core.persistence.api.entity.task.NotificationTask; +import org.apache.syncope.core.persistence.api.entity.task.TaskExec; +import org.apache.syncope.core.persistence.api.entity.user.UDerAttr; +import org.apache.syncope.core.persistence.api.entity.user.UPlainAttr; +import org.apache.syncope.core.persistence.api.entity.user.UVirAttr; +import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.provisioning.api.data.RoleDataBinder; +import org.apache.syncope.core.provisioning.api.data.UserDataBinder; +import org.apache.syncope.core.misc.ConnObjectUtil; +import org.apache.syncope.core.misc.search.SearchCondConverter; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.context.Context; +import org.apache.velocity.exception.VelocityException; +import org.apache.velocity.tools.ToolManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional(rollbackFor = { Throwable.class }) +public class NotificationManagerImpl implements NotificationManager { + + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(NotificationManager.class); + + public static final String MAIL_TEMPLATES = "mailTemplates/"; + + public static final String MAIL_TEMPLATE_HTML_SUFFIX = ".html.vm"; + + public static final String MAIL_TEMPLATE_TEXT_SUFFIX = ".txt.vm"; + + /** + * Notification DAO. + */ + @Autowired + private NotificationDAO notificationDAO; + + /** + * Configuration DAO. + */ + @Autowired + private ConfDAO confDAO; + + /** + * User DAO. + */ + @Autowired + private UserDAO userDAO; + + /** + * Role DAO. + */ + @Autowired + private RoleDAO roleDAO; + + /** + * User Search DAO. + */ + @Autowired + private SubjectSearchDAO searchDAO; + + /** + * Task DAO. + */ + @Autowired + private TaskDAO taskDAO; + + /** + * Velocity template engine. + */ + @Autowired + private VelocityEngine velocityEngine; + + /** + * Velocity tool manager. + */ + @Autowired + private ToolManager velocityToolManager; + + @Autowired + private EntitlementDAO entitlementDAO; + + @Autowired + private ConnObjectUtil connObjectUtil; + + @Autowired + private UserDataBinder userDataBinder; + + @Autowired + private RoleDataBinder roleDataBinder; + + @Autowired + private EntityFactory entityFactory; + + @Autowired + private AttributableUtilFactory attrUtilFactory; + + @Transactional(readOnly = true) + @Override + public long getMaxRetries() { + return confDAO.find("notification.maxRetries", "0").getValues().get(0).getLongValue(); + } + + /** + * Create a notification task. + * + * @param notification notification to take as model + * @param attributable the user this task is about + * @param model Velocity model + * @return notification task, fully populated + */ + private NotificationTask getNotificationTask( + final Notification notification, + final Attributable<?, ?, ?> attributable, + final Map<String, Object> model) { + + if (attributable != null) { + connObjectUtil.retrieveVirAttrValues(attributable, + attrUtilFactory.getInstance( + attributable instanceof User ? AttributableType.USER : AttributableType.ROLE)); + } + + final List<User> recipients = new ArrayList<>(); + + if (notification.getRecipients() != null) { + recipients.addAll(searchDAO.<User>search(RoleEntitlementUtil.getRoleKeys(entitlementDAO.findAll()), + SearchCondConverter.convert(notification.getRecipients()), + Collections.<OrderByClause>emptyList(), SubjectType.USER)); + } + + if (notification.isSelfAsRecipient() && attributable instanceof User) { + recipients.add((User) attributable); + } + + final Set<String> recipientEmails = new HashSet<>(); + final List<UserTO> recipientTOs = new ArrayList<>(recipients.size()); + for (User recipient : recipients) { + connObjectUtil.retrieveVirAttrValues(recipient, attrUtilFactory.getInstance(AttributableType.USER)); + + String email = getRecipientEmail(notification.getRecipientAttrType(), + notification.getRecipientAttrName(), recipient); + if (email == null) { + LOG.warn("{} cannot be notified: {} not found", recipient, notification.getRecipientAttrName()); + } else { + recipientEmails.add(email); + recipientTOs.add(userDataBinder.getUserTO(recipient)); + } + } + + if (notification.getStaticRecipients() != null) { + recipientEmails.addAll(notification.getStaticRecipients()); + } + + model.put("recipients", recipientTOs); + model.put("syncopeConf", this.findAllSyncopeConfs()); + model.put("events", notification.getEvents()); + + NotificationTask task = entityFactory.newEntity(NotificationTask.class); + task.setTraceLevel(notification.getTraceLevel()); + task.getRecipients().addAll(recipientEmails); + task.setSender(notification.getSender()); + task.setSubject(notification.getSubject()); + + String htmlBody = mergeTemplateIntoString( + MAIL_TEMPLATES + notification.getTemplate() + MAIL_TEMPLATE_HTML_SUFFIX, model); + String textBody = mergeTemplateIntoString( + MAIL_TEMPLATES + notification.getTemplate() + MAIL_TEMPLATE_TEXT_SUFFIX, model); + + task.setHtmlBody(htmlBody); + task.setTextBody(textBody); + + return task; + } + + private String mergeTemplateIntoString(final String templateLocation, final Map<String, Object> model) { + StringWriter result = new StringWriter(); + try { + Context velocityContext = createVelocityContext(model); + velocityEngine.mergeTemplate(templateLocation, SyncopeConstants.DEFAULT_ENCODING, velocityContext, result); + } catch (VelocityException e) { + LOG.error("Could not get mail body", e); + } catch (RuntimeException e) { + // ensure same behaviour as by using Spring VelocityEngineUtils.mergeTemplateIntoString() + throw e; + } catch (Exception e) { + LOG.error("Could not get mail body", e); + } + + return result.toString(); + } + + /** + * Create a Velocity Context for the given model, to be passed to the template for merging. + * + * @param model Velocity model + * @return Velocity context + */ + protected Context createVelocityContext(Map<String, Object> model) { + Context toolContext = velocityToolManager.createContext(); + return new VelocityContext(model, toolContext); + } + + @Override + public void createTasks( + final AuditElements.EventCategoryType type, + final String category, + final String subcategory, + final String event, + final Result condition, + final Object before, + final Object output, + final Object... input) { + + SubjectType subjectType = null; + Subject<?, ?, ?> subject = null; + + if (before instanceof UserTO) { + subjectType = SubjectType.USER; + subject = userDAO.find(((UserTO) before).getKey()); + } else if (output instanceof UserTO) { + subjectType = SubjectType.USER; + subject = userDAO.find(((UserTO) output).getKey()); + } else if (before instanceof RoleTO) { + subjectType = SubjectType.ROLE; + subject = roleDAO.find(((RoleTO) before).getKey()); + } else if (output instanceof RoleTO) { + subjectType = SubjectType.ROLE; + subject = roleDAO.find(((RoleTO) output).getKey()); + } + + LOG.debug("Search notification for [{}]{}", subjectType, subject); + + for (Notification notification : notificationDAO.findAll()) { + LOG.debug("Notification available user about {}", notification.getUserAbout()); + LOG.debug("Notification available role about {}", notification.getRoleAbout()); + if (notification.isActive()) { + + final Set<String> events = new HashSet<>(notification.getEvents()); + events.retainAll(Collections.<String>singleton(AuditLoggerName.buildEvent( + type, category, subcategory, event, condition))); + + if (events.isEmpty()) { + LOG.debug("No events found about {}", subject); + } else if (subjectType == null || subject == null + || notification.getUserAbout() == null || notification.getRoleAbout() == null + || searchDAO.matches(subject, + SearchCondConverter.convert(notification.getUserAbout()), subjectType) + || searchDAO.matches(subject, + SearchCondConverter.convert(notification.getRoleAbout()), subjectType)) { + + LOG.debug("Creating notification task for events {} about {}", events, subject); + + final Map<String, Object> model = new HashMap<>(); + model.put("type", type); + model.put("category", category); + model.put("subcategory", subcategory); + model.put("event", event); + model.put("condition", condition); + model.put("before", before); + model.put("output", output); + model.put("input", input); + + if (subject instanceof User) { + model.put("user", userDataBinder.getUserTO((User) subject)); + } else if (subject instanceof Role) { + model.put("role", roleDataBinder.getRoleTO((Role) subject)); + } + + taskDAO.save(getNotificationTask(notification, subject, model)); + } + } else { + LOG.debug("Notification {}, userAbout {}, roleAbout {} is deactivated, " + + "notification task will not be created", notification.getKey(), + notification.getUserAbout(), notification.getRoleAbout()); + } + } + } + + private String getRecipientEmail( + final IntMappingType recipientAttrType, final String recipientAttrName, final User user) { + + String email = null; + + switch (recipientAttrType) { + case Username: + email = user.getUsername(); + break; + + case UserPlainSchema: + UPlainAttr attr = user.getPlainAttr(recipientAttrName); + if (attr != null && !attr.getValuesAsStrings().isEmpty()) { + email = attr.getValuesAsStrings().get(0); + } + break; + + case UserVirtualSchema: + UVirAttr virAttr = user.getVirAttr(recipientAttrName); + if (virAttr != null && !virAttr.getValues().isEmpty()) { + email = virAttr.getValues().get(0); + } + break; + + case UserDerivedSchema: + UDerAttr derAttr = user.getDerAttr(recipientAttrName); + if (derAttr != null) { + email = derAttr.getValue(user.getPlainAttrs()); + } + break; + + default: + } + + return email; + } + + @Override + public TaskExec storeExec(final TaskExec execution) { + NotificationTask task = taskDAO.find(execution.getTask().getKey()); + task.addExec(execution); + task.setExecuted(true); + taskDAO.save(task); + // this flush call is needed to generate a value for the execution id + taskDAO.flush(); + return execution; + } + + @Override + public void setTaskExecuted(final Long taskId, final boolean executed) { + NotificationTask task = taskDAO.find(taskId); + task.setExecuted(executed); + taskDAO.save(task); + } + + @Override + public long countExecutionsWithStatus(final Long taskId, final String status) { + NotificationTask task = taskDAO.find(taskId); + long count = 0; + for (TaskExec taskExec : task.getExecs()) { + if (status == null) { + if (taskExec.getStatus() == null) { + count++; + } + } else if (status.equals(taskExec.getStatus())) { + count++; + } + } + return count; + } + + protected Map<String, String> findAllSyncopeConfs() { + Map<String, String> syncopeConfMap = new HashMap<>(); + for (PlainAttr attr : confDAO.get().getPlainAttrs()) { + syncopeConfMap.put(attr.getSchema().getKey(), attr.getValuesAsStrings().get(0)); + } + return syncopeConfMap; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/SpringVelocityResourceLoader.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/SpringVelocityResourceLoader.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/SpringVelocityResourceLoader.java new file mode 100644 index 0000000..f1b072b --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/SpringVelocityResourceLoader.java @@ -0,0 +1,84 @@ +/* + * 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.notification; + +import java.io.IOException; +import java.io.InputStream; +import org.apache.commons.collections.ExtendedProperties; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Velocity ResourceLoader adapter that loads via a Spring ResourceLoader. + * Similar to <tt>org.springframework.ui.velocity.SpringResourceLoader</tt> but more integrated with + * {@link VelocityEngineFactoryBean}. + */ +public class SpringVelocityResourceLoader extends ResourceLoader { + + private static final Logger LOG = LoggerFactory.getLogger(SpringVelocityResourceLoader.class); + + public static final String NAME = "spring"; + + public static final String SPRING_RESOURCE_LOADER_CLASS = "spring.resource.loader.class"; + + public static final String SPRING_RESOURCE_LOADER_CACHE = "spring.resource.loader.cache"; + + public static final String SPRING_RESOURCE_LOADER = "spring.resource.loader"; + + private org.springframework.core.io.ResourceLoader resourceLoader; + + @Override + public void init(ExtendedProperties configuration) { + this.resourceLoader = + (org.springframework.core.io.ResourceLoader) this.rsvc.getApplicationAttribute(SPRING_RESOURCE_LOADER); + if (this.resourceLoader == null) { + throw new IllegalArgumentException( + "'" + SPRING_RESOURCE_LOADER + "' application attribute must be present for SpringResourceLoader"); + } + + LOG.info("SpringResourceLoader for Velocity: using resource loader [" + this.resourceLoader + "]"); + } + + @Override + public InputStream getResourceStream(final String source) throws ResourceNotFoundException { + LOG.debug("Looking for Velocity resource with name [{}]", source); + + org.springframework.core.io.Resource resource = this.resourceLoader.getResource(source); + try { + return resource.getInputStream(); + } catch (IOException e) { + LOG.debug("Could not find Velocity resource: " + resource, e); + } + throw new ResourceNotFoundException("Could not find resource [" + source + "]"); + } + + @Override + public boolean isSourceModified(final Resource resource) { + return false; + } + + @Override + public long getLastModified(final Resource resource) { + return 0; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/VelocityEngineFactoryBean.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/VelocityEngineFactoryBean.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/VelocityEngineFactoryBean.java new file mode 100644 index 0000000..fae1504 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/VelocityEngineFactoryBean.java @@ -0,0 +1,105 @@ +/* + * 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.notification; + +import java.io.IOException; + +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.exception.VelocityException; +import org.apache.velocity.runtime.RuntimeConstants; +import org.apache.velocity.runtime.log.CommonsLogLogChute; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; + +/** + * Similar to Spring's equivalent (<tt>org.springframework.ui.velocity.VelocityEngineFactoryBean</tt>), does not + * implement {@link org.springframework.context.ResourceLoaderAware} thus allowing custom injection. + */ +public class VelocityEngineFactoryBean implements FactoryBean<VelocityEngine>, InitializingBean { + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + private boolean overrideLogging = true; + + private VelocityEngine velocityEngine; + + public ResourceLoader getResourceLoader() { + return resourceLoader; + } + + public void setResourceLoader(final ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public boolean isOverrideLogging() { + return overrideLogging; + } + + /** + * Configure Velocity to use Commons Logging (true by default). + * + * @param overrideLogging whether default Velocity logging should be overriden or not. + */ + public void setOverrideLogging(final boolean overrideLogging) { + this.overrideLogging = overrideLogging; + } + + private void createVelocityEngine() throws IOException, VelocityException { + velocityEngine = new VelocityEngine(); + + velocityEngine.setProperty( + RuntimeConstants.RESOURCE_LOADER, SpringVelocityResourceLoader.NAME); + velocityEngine.setProperty( + SpringVelocityResourceLoader.SPRING_RESOURCE_LOADER_CLASS, + SpringVelocityResourceLoader.class.getName()); + velocityEngine.setProperty( + SpringVelocityResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true"); + velocityEngine.setApplicationAttribute( + SpringVelocityResourceLoader.SPRING_RESOURCE_LOADER, getResourceLoader()); + + if (this.overrideLogging) { + velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new CommonsLogLogChute()); + } + + velocityEngine.init(); + } + + @Override + public void afterPropertiesSet() throws IOException, VelocityException { + createVelocityEngine(); + } + + @Override + public VelocityEngine getObject() { + return this.velocityEngine; + } + + @Override + public Class<? extends VelocityEngine> getObjectType() { + return VelocityEngine.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java new file mode 100644 index 0000000..83e14fa --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java @@ -0,0 +1,540 @@ +/* + * 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.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.syncope.common.lib.types.AuditElements; +import org.apache.syncope.common.lib.types.AuditElements.Result; +import org.apache.syncope.common.lib.types.MappingPurpose; +import org.apache.syncope.common.lib.types.PropagationMode; +import org.apache.syncope.common.lib.types.PropagationTaskExecStatus; +import org.apache.syncope.common.lib.types.TraceLevel; +import org.apache.syncope.core.persistence.api.dao.RoleDAO; +import org.apache.syncope.core.persistence.api.dao.TaskDAO; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.entity.AttributableUtilFactory; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.ExternalResource; +import org.apache.syncope.core.persistence.api.entity.Subject; +import org.apache.syncope.core.persistence.api.entity.task.PropagationTask; +import org.apache.syncope.core.persistence.api.entity.task.TaskExec; +import org.apache.syncope.core.provisioning.api.Connector; +import org.apache.syncope.core.provisioning.api.ConnectorFactory; +import org.apache.syncope.core.provisioning.api.TimeoutException; +import org.apache.syncope.core.provisioning.api.propagation.PropagationActions; +import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter; +import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor; +import org.apache.syncope.core.misc.AuditManager; +import org.apache.syncope.core.misc.spring.ApplicationContextProvider; +import org.apache.syncope.core.misc.ConnObjectUtil; +import org.apache.syncope.core.misc.ExceptionUtil; +import org.apache.syncope.core.provisioning.api.notification.NotificationManager; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeUtil; +import org.identityconnectors.framework.common.objects.ConnectorObject; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.Uid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(rollbackFor = { Throwable.class }) +public abstract class AbstractPropagationTaskExecutor implements PropagationTaskExecutor { + + /** + * Logger. + */ + protected static final Logger LOG = LoggerFactory.getLogger(PropagationTaskExecutor.class); + + /** + * Connector factory. + */ + @Autowired + protected ConnectorFactory connFactory; + + /** + * ConnObjectUtil. + */ + @Autowired + protected ConnObjectUtil connObjectUtil; + + /** + * User DAO. + */ + @Autowired + protected UserDAO userDAO; + + /** + * User DAO. + */ + @Autowired + protected RoleDAO roleDAO; + + /** + * Task DAO. + */ + @Autowired + protected TaskDAO taskDAO; + + /** + * Notification Manager. + */ + @Autowired + protected NotificationManager notificationManager; + + /** + * Audit Manager. + */ + @Autowired + protected AuditManager auditManager; + + @Autowired + protected AttributableUtilFactory attrUtilFactory; + + @Autowired + protected EntityFactory entityFactory; + + @Override + public TaskExec execute(final PropagationTask task) { + return execute(task, null); + } + + protected List<PropagationActions> getPropagationActions(final ExternalResource resource) { + List<PropagationActions> result = new ArrayList<>(); + + if (!resource.getPropagationActionsClassNames().isEmpty()) { + for (String className : resource.getPropagationActionsClassNames()) { + try { + Class<?> actionsClass = Class.forName(className); + result.add((PropagationActions) ApplicationContextProvider.getBeanFactory(). + createBean(actionsClass, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true)); + } catch (ClassNotFoundException e) { + LOG.error("Invalid PropagationAction class name '{}' for resource {}", resource, className, e); + } + } + } + + return result; + } + + public static void createOrUpdate( + final ObjectClass oclass, + final String accountId, + final Set<Attribute> attrs, + final String resource, + final PropagationMode propagationMode, + final ConnectorObject beforeObj, + final Connector connector, + final Set<String> propagationAttempted, + final ConnObjectUtil connObjectUtil) { + + // set of attributes to be propagated + final Set<Attribute> attributes = new HashSet<>(attrs); + + // check if there is any missing or null / empty mandatory attribute + List<Object> mandatoryAttrNames = new ArrayList<>(); + Attribute mandatoryMissing = AttributeUtil.find(MANDATORY_MISSING_ATTR_NAME, attrs); + if (mandatoryMissing != null) { + attributes.remove(mandatoryMissing); + + if (beforeObj == null) { + mandatoryAttrNames.addAll(mandatoryMissing.getValue()); + } + } + Attribute mandatoryNullOrEmpty = AttributeUtil.find(MANDATORY_NULL_OR_EMPTY_ATTR_NAME, attrs); + if (mandatoryNullOrEmpty != null) { + attributes.remove(mandatoryNullOrEmpty); + + mandatoryAttrNames.addAll(mandatoryNullOrEmpty.getValue()); + } + if (!mandatoryAttrNames.isEmpty()) { + throw new IllegalArgumentException( + "Not attempted because there are mandatory attributes without value(s): " + mandatoryAttrNames); + } + + if (beforeObj == null) { + LOG.debug("Create {} on {}", attributes, resource); + connector.create(propagationMode, oclass, attributes, null, propagationAttempted); + } else { + // 1. check if rename is really required + final Name newName = (Name) AttributeUtil.find(Name.NAME, attributes); + + LOG.debug("Rename required with value {}", newName); + + if (newName != null && newName.equals(beforeObj.getName()) + && !newName.getNameValue().equals(beforeObj.getUid().getUidValue())) { + + LOG.debug("Remote object name unchanged"); + attributes.remove(newName); + } + + // 2. check wether anything is actually needing to be propagated, i.e. if there is attribute + // difference between beforeObj - just read above from the connector - and the values to be propagated + Map<String, Attribute> originalAttrMap = connObjectUtil.toMap(beforeObj.getAttributes()); + Map<String, Attribute> updateAttrMap = connObjectUtil.toMap(attributes); + + // Only compare attribute from beforeObj that are also being updated + Set<String> skipAttrNames = originalAttrMap.keySet(); + skipAttrNames.removeAll(updateAttrMap.keySet()); + for (String attrName : new HashSet<>(skipAttrNames)) { + originalAttrMap.remove(attrName); + } + + Set<Attribute> originalAttrs = new HashSet<>(originalAttrMap.values()); + + if (originalAttrs.equals(attributes)) { + LOG.debug("Don't need to propagate anything: {} is equal to {}", originalAttrs, attributes); + } else { + LOG.debug("Attributes that would be updated {}", attributes); + + Set<Attribute> strictlyModified = new HashSet<>(); + for (Attribute attr : attributes) { + if (!originalAttrs.contains(attr)) { + strictlyModified.add(attr); + } + } + + // 3. provision entry + LOG.debug("Update {} on {}", strictlyModified, resource); + + connector.update(propagationMode, beforeObj.getObjectClass(), + beforeObj.getUid(), strictlyModified, null, propagationAttempted); + } + } + } + + protected void createOrUpdate( + final PropagationTask task, + final ConnectorObject beforeObj, + final Connector connector, + final Set<String> propagationAttempted) { + + createOrUpdate( + new ObjectClass(task.getObjectClassName()), + task.getAccountId(), + task.getAttributes(), + task.getResource().getKey(), + task.getResource().getPropagationMode(), + beforeObj, + connector, + propagationAttempted, + connObjectUtil); + } + + protected Subject<?, ?, ?> getSubject(final PropagationTask task) { + Subject<?, ?, ?> subject = null; + + if (task.getSubjectKey() != null) { + switch (task.getSubjectType()) { + case USER: + try { + subject = userDAO.authFetch(task.getSubjectKey()); + } catch (Exception e) { + LOG.error("Could not read user {}", task.getSubjectKey(), e); + } + break; + + case ROLE: + try { + subject = roleDAO.authFetch(task.getSubjectKey()); + } catch (Exception e) { + LOG.error("Could not read role {}", task.getSubjectKey(), e); + } + break; + + case MEMBERSHIP: + default: + } + } + + return subject; + } + + protected void delete(final PropagationTask task, final ConnectorObject beforeObj, + final Connector connector, final Set<String> propagationAttempted) { + + if (beforeObj == null) { + LOG.debug("{} not found on external resource: ignoring delete", task.getAccountId()); + } else { + /* + * We must choose here whether to + * a. actually delete the provided user / role from the external resource + * b. just update the provided user / role data onto the external resource + * + * (a) happens when either there is no user / role associated with the PropagationTask (this takes place + * when the task is generated via UserController.delete() / RoleController.delete()) or the provided updated + * user / role hasn't the current resource assigned (when the task is generated via + * UserController.update() / RoleController.update()). + * + * (b) happens when the provided updated user / role does have the current resource assigned (when the task + * is generated via UserController.update() / RoleController.updae()): this basically means that before such + * update, this user / role used to have the current resource assigned by more than one mean (for example, + * two different memberships with the same resource). + */ + Subject<?, ?, ?> subject = getSubject(task); + if (subject == null || !subject.getResourceNames().contains(task.getResource().getKey())) { + LOG.debug("Delete {} on {}", beforeObj.getUid(), task.getResource().getKey()); + + connector.delete( + task.getPropagationMode(), + beforeObj.getObjectClass(), + beforeObj.getUid(), + null, + propagationAttempted); + } else { + createOrUpdate(task, beforeObj, connector, propagationAttempted); + } + } + } + + @Override + public TaskExec execute(final PropagationTask task, final PropagationReporter reporter) { + final List<PropagationActions> actions = getPropagationActions(task.getResource()); + + final Date startDate = new Date(); + + final TaskExec execution = entityFactory.newEntity(TaskExec.class); + execution.setStatus(PropagationTaskExecStatus.CREATED.name()); + + String taskExecutionMessage = null; + String failureReason = null; + + // Flag to state whether any propagation has been attempted + Set<String> propagationAttempted = new HashSet<>(); + + ConnectorObject beforeObj = null; + ConnectorObject afterObj = null; + + Connector connector = null; + Result result; + try { + connector = connFactory.getConnector(task.getResource()); + + // Try to read remote object (user / group) BEFORE any actual operation + beforeObj = getRemoteObject(task, connector, false); + + for (PropagationActions action : actions) { + action.before(task, beforeObj); + } + + switch (task.getPropagationOperation()) { + case CREATE: + case UPDATE: + createOrUpdate(task, beforeObj, connector, propagationAttempted); + break; + + case DELETE: + delete(task, beforeObj, connector, propagationAttempted); + break; + + default: + } + + execution.setStatus(task.getPropagationMode() == PropagationMode.ONE_PHASE + ? PropagationTaskExecStatus.SUCCESS.name() + : PropagationTaskExecStatus.SUBMITTED.name()); + + LOG.debug("Successfully propagated to {}", task.getResource()); + result = Result.SUCCESS; + } catch (Exception e) { + result = Result.FAILURE; + LOG.error("Exception during provision on resource " + task.getResource().getKey(), e); + + if (e instanceof ConnectorException && e.getCause() != null) { + taskExecutionMessage = e.getCause().getMessage(); + if (e.getCause().getMessage() == null) { + failureReason = e.getMessage(); + } else { + failureReason = e.getMessage() + "\n\n Cause: " + e.getCause().getMessage().split("\n")[0]; + } + } else { + taskExecutionMessage = ExceptionUtil.getFullStackTrace(e); + if (e.getCause() == null) { + failureReason = e.getMessage(); + } else { + failureReason = e.getMessage() + "\n\n Cause: " + e.getCause().getMessage().split("\n")[0]; + } + } + + try { + execution.setStatus(task.getPropagationMode() == PropagationMode.ONE_PHASE + ? PropagationTaskExecStatus.FAILURE.name() + : PropagationTaskExecStatus.UNSUBMITTED.name()); + } catch (Exception wft) { + LOG.error("While executing KO action on {}", execution, wft); + } + + propagationAttempted.add(task.getPropagationOperation().name().toLowerCase()); + } finally { + // Try to read remote object (user / group) AFTER any actual operation + if (connector != null) { + try { + afterObj = getRemoteObject(task, connector, true); + } catch (Exception ignore) { + // ignore exception + LOG.error("Error retrieving after object", ignore); + } + } + + LOG.debug("Update execution for {}", task); + + execution.setStartDate(startDate); + execution.setMessage(taskExecutionMessage); + execution.setEndDate(new Date()); + + if (hasToBeregistered(task, execution)) { + if (propagationAttempted.isEmpty()) { + LOG.debug("No propagation attempted for {}", execution); + } else { + execution.setTask(task); + task.addExec(execution); + + LOG.debug("Execution finished: {}", execution); + } + + taskDAO.save(task); + + // this flush call is needed to generate a value for the execution id + taskDAO.flush(); + } + + if (reporter != null) { + reporter.onSuccessOrSecondaryResourceFailures( + task.getResource().getKey(), + PropagationTaskExecStatus.valueOf(execution.getStatus()), + failureReason, + beforeObj, + afterObj); + } + } + + for (PropagationActions action : actions) { + action.after(task, execution, afterObj); + } + + notificationManager.createTasks( + AuditElements.EventCategoryType.PROPAGATION, + task.getSubjectType().name().toLowerCase(), + task.getResource().getKey(), + task.getPropagationOperation().name().toLowerCase(), + result, + beforeObj, // searching for before object is too much expensive ... + new Object[] { execution, afterObj }, + task); + + auditManager.audit( + AuditElements.EventCategoryType.PROPAGATION, + task.getSubjectType().name().toLowerCase(), + task.getResource().getKey(), + task.getPropagationOperation().name().toLowerCase(), + result, + beforeObj, // searching for before object is too much expensive ... + new Object[] { execution, afterObj }, + task); + + return execution; + } + + @Override + public void execute(final Collection<PropagationTask> tasks) { + execute(tasks, null); + } + + @Override + public abstract void execute(Collection<PropagationTask> tasks, final PropagationReporter reporter); + + /** + * Check whether an execution has to be stored, for a given task. + * + * @param task execution's task + * @param execution to be decide whether to store or not + * @return true if execution has to be store, false otherwise + */ + protected boolean hasToBeregistered(final PropagationTask task, final TaskExec execution) { + boolean result; + + final boolean failed = !PropagationTaskExecStatus.valueOf(execution.getStatus()).isSuccessful(); + + switch (task.getPropagationOperation()) { + + case CREATE: + result = (failed && task.getResource().getCreateTraceLevel().ordinal() >= TraceLevel.FAILURES.ordinal()) + || task.getResource().getCreateTraceLevel() == TraceLevel.ALL; + break; + + case UPDATE: + result = (failed && task.getResource().getUpdateTraceLevel().ordinal() >= TraceLevel.FAILURES.ordinal()) + || task.getResource().getUpdateTraceLevel() == TraceLevel.ALL; + break; + + case DELETE: + result = (failed && task.getResource().getDeleteTraceLevel().ordinal() >= TraceLevel.FAILURES.ordinal()) + || task.getResource().getDeleteTraceLevel() == TraceLevel.ALL; + break; + + default: + result = false; + } + + return result; + } + + /** + * Get remote object for given task. + * + * @param connector connector facade proxy. + * @param task current propagation task. + * @param latest 'FALSE' to retrieve object using old accountId if not null. + * @return remote connector object. + */ + protected ConnectorObject getRemoteObject(final PropagationTask task, final Connector connector, + final boolean latest) { + + String accountId = latest || task.getOldAccountId() == null + ? task.getAccountId() + : task.getOldAccountId(); + + ConnectorObject obj = null; + try { + obj = connector.getObject(task.getPropagationMode(), + task.getPropagationOperation(), + new ObjectClass(task.getObjectClassName()), + new Uid(accountId), + connector.getOperationOptions(attrUtilFactory.getInstance(task.getSubjectType()). + getMappingItems(task.getResource(), MappingPurpose.PROPAGATION))); + } catch (TimeoutException toe) { + LOG.debug("Request timeout", toe); + throw toe; + } catch (RuntimeException ignore) { + LOG.debug("While resolving {}", accountId, ignore); + } + + return obj; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DBPasswordPropagationActions.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DBPasswordPropagationActions.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DBPasswordPropagationActions.java new file mode 100644 index 0000000..fd4b3dc --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DBPasswordPropagationActions.java @@ -0,0 +1,120 @@ +/* + * 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.HashSet; +import java.util.Iterator; +import java.util.Set; +import org.apache.syncope.common.lib.types.AttributableType; +import org.apache.syncope.common.lib.types.CipherAlgorithm; +import org.apache.syncope.common.lib.types.ConnConfProperty; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.entity.ConnInstance; +import org.apache.syncope.core.persistence.api.entity.task.PropagationTask; +import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor; +import org.identityconnectors.common.security.GuardedString; +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.ConnectorObject; +import org.identityconnectors.framework.common.objects.OperationalAttributes; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +/** + * Propagate a non-cleartext password out to a resource, if the PropagationManager has not already + * added a password. The CipherAlgorithm associated with the password must match the password + * cipher algorithm property of the DB Connector. + */ +public class DBPasswordPropagationActions extends DefaultPropagationActions { + + private static final String CLEARTEXT = "CLEARTEXT"; + + @Autowired + private UserDAO userDAO; + + @Transactional(readOnly = true) + @Override + public void before(final PropagationTask task, final ConnectorObject beforeObj) { + super.before(task, beforeObj); + + if (AttributableType.USER == task.getSubjectType()) { + User user = userDAO.find(task.getSubjectKey()); + + if (user != null && user.getPassword() != null) { + Attribute missing = AttributeUtil.find( + PropagationTaskExecutor.MANDATORY_MISSING_ATTR_NAME, + task.getAttributes()); + + ConnInstance connInstance = task.getResource().getConnector(); + if (missing != null && missing.getValue() != null && missing.getValue().size() == 1 + && missing.getValue().get(0).equals(OperationalAttributes.PASSWORD_NAME) + && cipherAlgorithmMatches(getCipherAlgorithm(connInstance), user.getCipherAlgorithm())) { + + Attribute passwordAttribute = AttributeBuilder.buildPassword( + new GuardedString(user.getPassword().toCharArray())); + + Set<Attribute> attributes = new HashSet<Attribute>(task.getAttributes()); + attributes.add(passwordAttribute); + attributes.remove(missing); + + Attribute hashedPasswordAttribute = AttributeBuilder.build( + AttributeUtil.createSpecialName("HASHED_PASSWORD"), Boolean.TRUE); + attributes.add(hashedPasswordAttribute); + + task.setAttributes(attributes); + } + } + } + } + + private String getCipherAlgorithm(ConnInstance connInstance) { + String cipherAlgorithm = CLEARTEXT; + for (Iterator<ConnConfProperty> propertyIterator = connInstance.getConfiguration().iterator(); + propertyIterator.hasNext();) { + + ConnConfProperty property = propertyIterator.next(); + if ("cipherAlgorithm".equals(property.getSchema().getName()) + && property.getValues() != null && !property.getValues().isEmpty()) { + + return (String) property.getValues().get(0); + } + } + return cipherAlgorithm; + } + + private boolean cipherAlgorithmMatches(String connectorAlgorithm, CipherAlgorithm userAlgorithm) { + if (userAlgorithm == null) { + return false; + } + + if (connectorAlgorithm.equals(userAlgorithm.name())) { + return true; + } + + // Special check for "SHA" (user sync'd from LDAP) + if ("SHA1".equals(connectorAlgorithm) && "SHA".equals(userAlgorithm.name())) { + return true; + } + + return false; + } + +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationActions.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationActions.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationActions.java new file mode 100644 index 0000000..379326d --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationActions.java @@ -0,0 +1,38 @@ +/* + * 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 org.apache.syncope.core.persistence.api.entity.task.PropagationTask; +import org.apache.syncope.core.persistence.api.entity.task.TaskExec; +import org.apache.syncope.core.provisioning.api.propagation.PropagationActions; +import org.identityconnectors.framework.common.objects.ConnectorObject; + +/** + * Default (empty) implementation of PropagationActions. + */ +public abstract class DefaultPropagationActions implements PropagationActions { + + @Override + public void before(final PropagationTask task, final ConnectorObject beforeObj) { + } + + @Override + public void after(final PropagationTask task, final TaskExec execution, final ConnectorObject afterObj) { + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java new file mode 100644 index 0000000..df655a4 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationReporter.java @@ -0,0 +1,94 @@ +/* + * 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.List; +import org.apache.syncope.common.lib.to.PropagationStatus; +import org.apache.syncope.common.lib.types.PropagationTaskExecStatus; +import org.apache.syncope.core.persistence.api.entity.task.PropagationTask; +import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter; +import org.apache.syncope.core.misc.ConnObjectUtil; +import org.identityconnectors.framework.common.objects.ConnectorObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +public class DefaultPropagationReporter implements PropagationReporter { + + protected static final Logger LOG = LoggerFactory.getLogger(DefaultPropagationReporter.class); + + @Autowired + protected ConnObjectUtil connObjectUtil; + + protected final List<PropagationStatus> statuses = new ArrayList<>(); + + @Override + public void onSuccessOrSecondaryResourceFailures(final String resource, + final PropagationTaskExecStatus executionStatus, + final String failureReason, final ConnectorObject beforeObj, final ConnectorObject afterObj) { + + final PropagationStatus propagation = new PropagationStatus(); + propagation.setResource(resource); + propagation.setStatus(executionStatus); + propagation.setFailureReason(failureReason); + + if (beforeObj != null) { + propagation.setBeforeObj(connObjectUtil.getConnObjectTO(beforeObj)); + } + + if (afterObj != null) { + propagation.setAfterObj(connObjectUtil.getConnObjectTO(afterObj)); + } + + statuses.add(propagation); + } + + private boolean containsPropagationStatusTO(final String resourceName) { + for (PropagationStatus status : statuses) { + if (resourceName.equals(status.getResource())) { + return true; + } + } + return false; + } + + @Override + public void onPrimaryResourceFailure(final List<PropagationTask> tasks) { + final String failedResource = statuses.get(statuses.size() - 1).getResource(); + + LOG.debug("Propagation error: {} primary resource failed to propagate", failedResource); + + for (PropagationTask propagationTask : tasks) { + if (!containsPropagationStatusTO(propagationTask.getResource().getKey())) { + final PropagationStatus propagationStatusTO = new PropagationStatus(); + propagationStatusTO.setResource(propagationTask.getResource().getKey()); + propagationStatusTO.setStatus(PropagationTaskExecStatus.FAILURE); + propagationStatusTO.setFailureReason( + "Propagation error: " + failedResource + " primary resource failed to propagate."); + statuses.add(propagationStatusTO); + } + } + } + + @Override + public List<PropagationStatus> getStatuses() { + return statuses; + } +} http://git-wip-us.apache.org/repos/asf/syncope/blob/d30c8526/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java ---------------------------------------------------------------------- diff --git a/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java new file mode 100644 index 0000000..b3dfc69 --- /dev/null +++ b/syncope620/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java @@ -0,0 +1,113 @@ +/* + * 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.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.jexl2.JexlContext; +import org.apache.commons.jexl2.MapContext; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.types.AttributableType; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.entity.role.Role; +import org.apache.syncope.core.persistence.api.entity.task.PropagationTask; +import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.misc.jexl.JexlUtil; +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.ConnectorObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +/** + * Simple action for propagating role memberships to LDAP groups, when the same resource is configured for both users + * and roles. + * + * @see org.apache.syncope.core.sync.impl.LDAPMembershipSyncActions + */ +public class LDAPMembershipPropagationActions extends DefaultPropagationActions { + + protected static final Logger LOG = LoggerFactory.getLogger(LDAPMembershipPropagationActions.class); + + @Autowired + protected UserDAO userDAO; + + /** + * Allows easy subclassing for the ConnId AD connector bundle. + * + * @return the name of the attribute used to keep track of group memberships + */ + protected String getGroupMembershipAttrName() { + return "ldapGroups"; + } + + @Transactional(readOnly = true) + @Override + public void before(final PropagationTask task, final ConnectorObject beforeObj) { + super.before(task, beforeObj); + + if (AttributableType.USER == task.getSubjectType() && task.getResource().getRmapping() != null) { + User user = userDAO.find(task.getSubjectKey()); + if (user != null) { + List<String> roleAccountLinks = new ArrayList<>(); + for (Role role : user.getRoles()) { + if (role.getResourceNames().contains(task.getResource().getKey()) + && StringUtils.isNotBlank(task.getResource().getRmapping().getAccountLink())) { + + LOG.debug("Evaluating accountLink for {}", role); + + final JexlContext jexlContext = new MapContext(); + JexlUtil.addFieldsToContext(role, jexlContext); + JexlUtil.addAttrsToContext(role.getPlainAttrs(), jexlContext); + JexlUtil.addDerAttrsToContext(role.getDerAttrs(), role.getPlainAttrs(), jexlContext); + + final String roleAccountLink = + JexlUtil.evaluate(task.getResource().getRmapping().getAccountLink(), jexlContext); + LOG.debug("AccountLink for {} is '{}'", role, roleAccountLink); + if (StringUtils.isNotBlank(roleAccountLink)) { + roleAccountLinks.add(roleAccountLink); + } + } + } + LOG.debug("Role accountLinks to propagate for membership: {}", roleAccountLinks); + + Set<Attribute> attributes = new HashSet<Attribute>(task.getAttributes()); + + Set<String> groups = new HashSet<String>(roleAccountLinks); + Attribute ldapGroups = AttributeUtil.find(getGroupMembershipAttrName(), attributes); + + if (ldapGroups != null) { + for (Object obj : ldapGroups.getValue()) { + groups.add(obj.toString()); + } + } + + attributes.add(AttributeBuilder.build(getGroupMembershipAttrName(), groups)); + task.setAttributes(attributes); + } + } else { + LOG.debug("Not about user, or role mapping missing for resource: not doing anything"); + } + } +}
