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.pushpull;
20  
21  import java.util.ArrayList;
22  import java.util.Collections;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Optional;
27  import java.util.Set;
28  import java.util.concurrent.ConcurrentHashMap;
29  import org.apache.syncope.common.lib.request.AnyUR;
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.EntityTO;
33  import org.apache.syncope.common.lib.to.GroupTO;
34  import org.apache.syncope.common.lib.to.Provision;
35  import org.apache.syncope.common.lib.to.ProvisioningReport;
36  import org.apache.syncope.common.lib.types.AnyTypeKind;
37  import org.apache.syncope.common.lib.types.PatchOperation;
38  import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
39  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
40  import org.apache.syncope.core.provisioning.api.Connector;
41  import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
42  import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
43  import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
44  import org.apache.syncope.core.provisioning.api.rules.PullMatch;
45  import org.apache.syncope.core.spring.implementation.InstanceScope;
46  import org.apache.syncope.core.spring.implementation.SyncopeImplementation;
47  import org.identityconnectors.framework.common.objects.Attribute;
48  import org.identityconnectors.framework.common.objects.ConnectorObject;
49  import org.identityconnectors.framework.common.objects.ObjectClass;
50  import org.identityconnectors.framework.common.objects.OperationOptionsBuilder;
51  import org.identityconnectors.framework.common.objects.SyncDelta;
52  import org.quartz.JobExecutionException;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  import org.springframework.beans.factory.annotation.Autowired;
56  import org.springframework.transaction.annotation.Propagation;
57  import org.springframework.transaction.annotation.Transactional;
58  
59  /**
60   * Simple action for pulling LDAP groups memberships to Syncope group memberships, when the same resource is
61   * configured for both users and groups.
62   *
63   * @see org.apache.syncope.core.provisioning.java.propagation.LDAPMembershipPropagationActions
64   */
65  @SyncopeImplementation(scope = InstanceScope.PER_CONTEXT)
66  public class LDAPMembershipPullActions implements PullActions {
67  
68      protected static final Logger LOG = LoggerFactory.getLogger(LDAPMembershipPullActions.class);
69  
70      @Autowired
71      protected AnyTypeDAO anyTypeDAO;
72  
73      @Autowired
74      protected GroupDAO groupDAO;
75  
76      @Autowired
77      protected InboundMatcher inboundMatcher;
78  
79      @Autowired
80      protected UserProvisioningManager userProvisioningManager;
81  
82      protected final Map<String, Set<String>> membershipsBefore = new ConcurrentHashMap<>();
83  
84      protected final Map<String, Set<String>> membershipsAfter = new ConcurrentHashMap<>();
85  
86      /**
87       * Allows easy subclassing for the ConnId AD connector bundle.
88       *
89       * @param connector A Connector instance to query for the groupMemberAttribute property name
90       * @return the name of the attribute used to keep track of group memberships
91       */
92      protected String getGroupMembershipAttrName(final Connector connector) {
93          return connector.getConnInstance().getConf().stream().
94                  filter(property -> "groupMemberAttribute".equals(property.getSchema().getName())
95                  && !property.getValues().isEmpty()).findFirst().
96                  map(groupMembership -> (String) groupMembership.getValues().get(0)).
97                  orElse("uniquemember");
98      }
99  
100     /**
101      * Read values of attribute returned by getGroupMembershipAttrName(); if not present in the given delta, perform an
102      * additional read on the underlying connector.
103      *
104      * @param delta representing the pulling group
105      * @param connector associated to the current resource
106      * @return value of attribute returned by
107      * {@link #getGroupMembershipAttrName}
108      */
109     protected List<Object> getMembAttrValues(final SyncDelta delta, final Connector connector) {
110         String groupMemberName = getGroupMembershipAttrName(connector);
111 
112         // first, try to read the configured attribute from delta, returned by the ongoing pull
113         Attribute membAttr = delta.getObject().getAttributeByName(groupMemberName);
114         // if not found, perform an additional read on the underlying connector for the same connector object
115         if (membAttr == null) {
116             ConnectorObject remoteObj = connector.getObject(
117                     ObjectClass.GROUP,
118                     delta.getUid(),
119                     false,
120                     new OperationOptionsBuilder().setAttributesToGet(groupMemberName).build());
121             if (remoteObj == null) {
122                 LOG.debug("Object for '{}' not found", delta.getUid().getUidValue());
123             } else {
124                 membAttr = remoteObj.getAttributeByName(groupMemberName);
125             }
126         }
127 
128         return membAttr == null || membAttr.getValue() == null
129                 ? List.of()
130                 : membAttr.getValue();
131     }
132 
133     /**
134      * Keep track of members of the group being updated before actual update takes place.
135      * This is not needed on
136      * <ul>
137      * <li>{@link #beforeProvision} because the pulling group does not exist yet on Syncope</li>
138      * <li>{@link #beforeDelete} because group delete cascades as membership removal for all users involved</li>
139      * </ul>
140      *
141      * {@inheritDoc}
142      */
143     @Transactional(readOnly = true)
144     @Override
145     public void beforeUpdate(
146             final ProvisioningProfile<?, ?> profile,
147             final SyncDelta delta,
148             final EntityTO entity,
149             final AnyUR anyUR) throws JobExecutionException {
150 
151         if (!(entity instanceof GroupTO)) {
152             PullActions.super.beforeUpdate(profile, delta, entity, anyUR);
153             return;
154         }
155 
156         groupDAO.findUMemberships(groupDAO.find(entity.getKey())).forEach(uMembership -> {
157             Set<String> memb = membershipsBefore.computeIfAbsent(
158                     uMembership.getLeftEnd().getKey(),
159                     k -> Collections.synchronizedSet(new HashSet<>()));
160             memb.add(entity.getKey());
161         });
162     }
163 
164     /**
165      * Keep track of members of the group being updated after actual update took place.
166      * {@inheritDoc}
167      */
168     @Override
169     public void after(
170             final ProvisioningProfile<?, ?> profile,
171             final SyncDelta delta,
172             final EntityTO entity,
173             final ProvisioningReport result) throws JobExecutionException {
174 
175         if (!(entity instanceof GroupTO)) {
176             PullActions.super.after(profile, delta, entity, result);
177             return;
178         }
179 
180         Optional<Provision> provision = profile.getTask().getResource().
181                 getProvisionByAnyType(AnyTypeKind.USER.name()).filter(p -> p.getMapping() != null);
182         if (provision.isEmpty()) {
183             PullActions.super.after(profile, delta, entity, result);
184             return;
185         }
186 
187         getMembAttrValues(delta, profile.getConnector()).forEach(membValue -> {
188             Optional<PullMatch> match = inboundMatcher.match(
189                     anyTypeDAO.findUser(),
190                     membValue.toString(),
191                     profile.getTask().getResource(),
192                     profile.getConnector());
193             if (match.isPresent()) {
194                 Set<String> memb = membershipsAfter.computeIfAbsent(
195                         match.get().getAny().getKey(),
196                         k -> Collections.synchronizedSet(new HashSet<>()));
197                 memb.add(entity.getKey());
198             } else {
199                 LOG.warn("Could not find matching user for {}", membValue);
200             }
201         });
202     }
203 
204     @Transactional(propagation = Propagation.REQUIRES_NEW)
205     @Override
206     public void afterAll(final ProvisioningProfile<?, ?> profile) throws JobExecutionException {
207         List<UserUR> updateReqs = new ArrayList<>();
208 
209         membershipsAfter.forEach((user, groups) -> {
210             UserUR userUR = new UserUR();
211             userUR.setKey(user);
212             updateReqs.add(userUR);
213 
214             groups.stream().forEach(group -> {
215                 Set<String> before = membershipsBefore.get(user);
216                 if (before == null || !before.contains(group)) {
217                     userUR.getMemberships().add(new MembershipUR.Builder(group).
218                             operation(PatchOperation.ADD_REPLACE).
219                             build());
220                 }
221             });
222         });
223 
224         membershipsBefore.forEach((user, groups) -> {
225             UserUR userUR = updateReqs.stream().
226                     filter(req -> user.equals(req.getKey())).findFirst().
227                     orElseGet(() -> {
228                         UserUR req = new UserUR.Builder(user).build();
229                         updateReqs.add(req);
230                         return req;
231                     });
232 
233             groups.forEach(group -> {
234                 Set<String> after = membershipsAfter.get(user);
235                 if (after == null || !after.contains(group)) {
236                     userUR.getMemberships().add(new MembershipUR.Builder(group).
237                             operation(PatchOperation.DELETE).
238                             build());
239                 }
240             });
241         });
242 
243         membershipsAfter.clear();
244         membershipsBefore.clear();
245 
246         String context = "PullTask " + profile.getTask().getKey() + " '" + profile.getTask().getName() + "'";
247         updateReqs.stream().filter(req -> !req.isEmpty()).forEach(req -> {
248             LOG.debug("About to update memberships for User {}", req.getKey());
249             userProvisioningManager.update(req, true, profile.getExecutor(), context);
250         });
251     }
252 }