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.provisioning.java.propagation;
20  
21  import java.util.HashSet;
22  import java.util.List;
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.Set;
26  import java.util.TreeSet;
27  import org.apache.commons.jexl3.JexlContext;
28  import org.apache.commons.jexl3.MapContext;
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.syncope.common.lib.request.MembershipUR;
31  import org.apache.syncope.common.lib.request.UserUR;
32  import org.apache.syncope.common.lib.to.Provision;
33  import org.apache.syncope.common.lib.types.AnyTypeKind;
34  import org.apache.syncope.common.lib.types.PatchOperation;
35  import org.apache.syncope.common.lib.types.ResourceOperation;
36  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
37  import org.apache.syncope.core.persistence.api.dao.UserDAO;
38  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
39  import org.apache.syncope.core.persistence.api.entity.group.Group;
40  import org.apache.syncope.core.persistence.api.entity.task.PropagationData;
41  import org.apache.syncope.core.persistence.api.entity.user.User;
42  import org.apache.syncope.core.provisioning.api.DerAttrHandler;
43  import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
44  import org.apache.syncope.core.provisioning.api.propagation.PropagationActions;
45  import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
46  import org.apache.syncope.core.spring.implementation.InstanceScope;
47  import org.apache.syncope.core.spring.implementation.SyncopeImplementation;
48  import org.identityconnectors.framework.common.objects.AttributeBuilder;
49  import org.identityconnectors.framework.common.objects.AttributeDeltaBuilder;
50  import org.identityconnectors.framework.common.objects.AttributeDeltaUtil;
51  import org.identityconnectors.framework.common.objects.AttributeUtil;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  import org.springframework.beans.factory.annotation.Autowired;
55  import org.springframework.transaction.annotation.Transactional;
56  
57  /**
58   * Simple action for propagating group memberships to LDAP groups, when the same resource is configured for both users
59   * and groups.
60   *
61   * @see org.apache.syncope.core.provisioning.java.pushpull.LDAPMembershipPullActions
62   */
63  @SyncopeImplementation(scope = InstanceScope.PER_CONTEXT)
64  public class LDAPMembershipPropagationActions implements PropagationActions {
65  
66      protected static final Logger LOG = LoggerFactory.getLogger(LDAPMembershipPropagationActions.class);
67  
68      @Autowired
69      protected DerAttrHandler derAttrHandler;
70  
71      @Autowired
72      protected UserDAO userDAO;
73  
74      @Autowired
75      protected GroupDAO groupDAO;
76  
77      /**
78       * Allows easy subclassing for the ConnId AD connector bundle.
79       *
80       * @return the name of the attribute used to keep track of group memberships
81       */
82      protected String getGroupMembershipAttrName() {
83          return "ldapGroups";
84      }
85  
86      protected String evaluateGroupConnObjectLink(final String connObjectLinkTemplate, final Group group) {
87          LOG.debug("Evaluating connObjectLink for {}", group);
88  
89          JexlContext jexlContext = new MapContext();
90          JexlUtils.addFieldsToContext(group, jexlContext);
91          JexlUtils.addPlainAttrsToContext(group.getPlainAttrs(), jexlContext);
92          JexlUtils.addDerAttrsToContext(group, derAttrHandler, jexlContext);
93  
94          return JexlUtils.evaluate(connObjectLinkTemplate, jexlContext).toString();
95      }
96  
97      protected void buildManagedGroupConnObjectLinks(
98              final ExternalResource resource,
99              final String connObjectLinkTemplate,
100             final Set<String> connObjectLinks) {
101 
102         List<Group> managedGroups = groupDAO.findByResource(resource);
103         managedGroups.forEach(group -> connObjectLinks.add(evaluateGroupConnObjectLink(connObjectLinkTemplate, group)));
104     }
105 
106     @Transactional(readOnly = true)
107     @Override
108     public void before(final PropagationTaskInfo taskInfo) {
109         if (AnyTypeKind.USER != taskInfo.getAnyTypeKind() || taskInfo.getOperation() == ResourceOperation.DELETE) {
110             return;
111         }
112 
113         taskInfo.getResource().getProvisionByAnyType(AnyTypeKind.GROUP.name()).
114                 map(Provision::getMapping).
115                 filter(mapping -> StringUtils.isNotBlank(mapping.getConnObjectLink())).ifPresentOrElse(mapping -> {
116 
117             User user = userDAO.find(taskInfo.getEntityKey());
118             Set<String> groups = new HashSet<>();
119 
120             // for each user group assigned to the resource of this task, compute and add the group's 
121             // connector object link
122             userDAO.findAllGroupKeys(user).stream().
123                     map(groupDAO::find).
124                     filter(group -> group.getResources().contains(taskInfo.getResource())).
125                     forEach(group -> {
126                         String groupConnObjectLink = evaluateGroupConnObjectLink(
127                                 mapping.getConnObjectLink(), group);
128 
129                         LOG.debug("ConnObjectLink for {} is '{}'", group, groupConnObjectLink);
130                         if (StringUtils.isNotBlank(groupConnObjectLink)) {
131                             groups.add(groupConnObjectLink);
132                         }
133                     });
134             LOG.debug("Group connObjectLinks to propagate for membership: {}", groups);
135 
136             PropagationData data = taskInfo.getPropagationData();
137 
138             // if groups were defined by resource mapping, take their values and clear up
139             Optional.ofNullable(AttributeUtil.find(getGroupMembershipAttrName(), data.getAttributes())).
140                     ifPresent(ldapGroups -> {
141                         Optional.ofNullable(ldapGroups.getValue()).
142                                 ifPresent(value -> value.forEach(obj -> groups.add(obj.toString())));
143 
144                         data.getAttributes().remove(ldapGroups);
145                     });
146             LOG.debug("Group connObjectLinks after including the ones from mapping: {}", groups);
147 
148             // take groups already assigned from beforeObj and include them too
149             taskInfo.getBeforeObj().
150                     map(beforeObj -> beforeObj.getAttributeByName(getGroupMembershipAttrName())).
151                     filter(Objects::nonNull).
152                     ifPresent(beforeLdapGroups -> {
153                         Set<String> connObjectLinks = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
154                         buildManagedGroupConnObjectLinks(
155                                 taskInfo.getResource(),
156                                 mapping.getConnObjectLink(),
157                                 connObjectLinks);
158 
159                         LOG.debug("Memberships not managed by Syncope: {}", beforeLdapGroups);
160                         beforeLdapGroups.getValue().stream().
161                                 filter(value -> !connObjectLinks.contains(String.valueOf(value))).
162                                 forEach(value -> groups.add(String.valueOf(value)));
163                     });
164 
165             LOG.debug("Adding Group connObjectLinks to attributes: {}={}", getGroupMembershipAttrName(), groups);
166             data.getAttributes().add(AttributeBuilder.build(getGroupMembershipAttrName(), groups));
167 
168             if (data.getAttributeDeltas() != null && taskInfo.getUpdateRequest() != null) {
169                 Set<String> groupsToAdd = new HashSet<>();
170                 Set<String> groupsToRemove = new HashSet<>();
171 
172                 // if groups were added or removed by last update, compute and add the group's connector object link
173                 for (MembershipUR memb : ((UserUR) taskInfo.getUpdateRequest()).getMemberships()) {
174                     String connObjectLink = evaluateGroupConnObjectLink(
175                             mapping.getConnObjectLink(),
176                             groupDAO.find(memb.getGroup()));
177                     if (memb.getOperation() == PatchOperation.ADD_REPLACE) {
178                         groupsToAdd.add(connObjectLink);
179                     } else {
180                         groupsToRemove.add(connObjectLink);
181                     }
182                 }
183 
184                 // if groups were already considered, take their values and clear up
185                 Optional.ofNullable(
186                         AttributeDeltaUtil.find(getGroupMembershipAttrName(), data.getAttributeDeltas())).
187                         ifPresent(ldapGroups -> {
188                             Optional.ofNullable(ldapGroups.getValuesToAdd()).
189                                     ifPresent(value -> value.forEach(obj -> groupsToAdd.add(obj.toString())));
190                             Optional.ofNullable(ldapGroups.getValuesToRemove()).
191                                     ifPresent(value -> value.forEach(obj -> groupsToRemove.add(obj.toString())));
192 
193                             data.getAttributeDeltas().remove(ldapGroups);
194                         });
195 
196                 if (!groupsToAdd.isEmpty() || !groupsToRemove.isEmpty()) {
197                     LOG.debug("Adding Group connObjectLinks to attribute deltas: {}={},{}",
198                             getGroupMembershipAttrName(), groupsToAdd, groupsToRemove);
199                     data.getAttributeDeltas().add(
200                             AttributeDeltaBuilder.build(getGroupMembershipAttrName(), groupsToAdd,
201                                     groupsToRemove));
202                 }
203             }
204         }, () -> LOG.debug("Not about user, or group mapping missing for resource: not doing anything"));
205     }
206 }