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.ext.scimv2.cxf.service;
20  
21  import java.util.HashSet;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.stream.Collectors;
26  import javax.ws.rs.core.Response;
27  import javax.ws.rs.core.Response.ResponseBuilder;
28  import org.apache.commons.jexl3.MapContext;
29  import org.apache.commons.lang3.ArrayUtils;
30  import org.apache.commons.lang3.BooleanUtils;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.syncope.common.lib.AnyOperations;
33  import org.apache.syncope.common.lib.SyncopeConstants;
34  import org.apache.syncope.common.lib.request.MembershipUR;
35  import org.apache.syncope.common.lib.request.UserUR;
36  import org.apache.syncope.common.lib.to.GroupTO;
37  import org.apache.syncope.common.lib.to.ProvisioningResult;
38  import org.apache.syncope.common.lib.to.UserTO;
39  import org.apache.syncope.common.lib.types.PatchOperation;
40  import org.apache.syncope.core.logic.GroupLogic;
41  import org.apache.syncope.core.logic.SCIMDataBinder;
42  import org.apache.syncope.core.logic.UserLogic;
43  import org.apache.syncope.core.logic.scim.SCIMConfManager;
44  import org.apache.syncope.core.persistence.api.dao.AnyDAO;
45  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
46  import org.apache.syncope.core.persistence.api.dao.UserDAO;
47  import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
48  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
49  import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
50  import org.apache.syncope.ext.scimv2.api.BadRequestException;
51  import org.apache.syncope.ext.scimv2.api.data.ListResponse;
52  import org.apache.syncope.ext.scimv2.api.data.Member;
53  import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
54  import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOp;
55  import org.apache.syncope.ext.scimv2.api.data.SCIMSearchRequest;
56  import org.apache.syncope.ext.scimv2.api.service.SCIMGroupService;
57  import org.apache.syncope.ext.scimv2.api.type.ErrorType;
58  import org.apache.syncope.ext.scimv2.api.type.PatchOp;
59  import org.apache.syncope.ext.scimv2.api.type.Resource;
60  import org.apache.syncope.ext.scimv2.api.type.SortOrder;
61  import org.springframework.util.CollectionUtils;
62  
63  public class SCIMGroupServiceImpl extends AbstractSCIMService<SCIMGroup> implements SCIMGroupService {
64  
65      public SCIMGroupServiceImpl(
66              final UserDAO userDAO,
67              final GroupDAO groupDAO,
68              final UserLogic userLogic,
69              final GroupLogic groupLogic,
70              final SCIMDataBinder binder,
71              final SCIMConfManager confManager) {
72  
73          super(userDAO, groupDAO, userLogic, groupLogic, binder, confManager);
74      }
75  
76      private void changeMembership(final String user, final String group, final PatchOp patchOp) {
77          UserUR req = new UserUR.Builder(user).
78                  membership(new MembershipUR.Builder(group).operation(patchOp == PatchOp.remove
79                          ? PatchOperation.DELETE : PatchOperation.ADD_REPLACE).build()).
80                  build();
81          try {
82              userLogic.update(req, false);
83          } catch (Exception e) {
84              LOG.error("While applying {} on membership of {} to {}", patchOp, group, user, e);
85          }
86      }
87  
88      @Override
89      public Response create(final SCIMGroup group) {
90          // first create group, no members assigned
91          ProvisioningResult<GroupTO> result = groupLogic.create(binder.toGroupCR(group), false);
92  
93          // then assign members
94          group.getMembers().forEach(member -> changeMembership(
95                  member.getValue(), result.getEntity().getKey(), PatchOp.add));
96  
97          return createResponse(
98                  result.getEntity().getKey(),
99                  binder.toSCIMGroup(
100                         result.getEntity(),
101                         uriInfo.getAbsolutePathBuilder().path(result.getEntity().getKey()).build().toASCIIString(),
102                         List.of(),
103                         List.of()));
104     }
105 
106     @Override
107     public SCIMGroup get(
108             final String id,
109             final String attributes,
110             final String excludedAttributes) {
111 
112         return binder.toSCIMGroup(
113                 groupLogic.read(id),
114                 uriInfo.getAbsolutePathBuilder().build().toASCIIString(),
115                 List.of(ArrayUtils.nullToEmpty(StringUtils.split(attributes, ','))),
116                 List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
117     }
118 
119     private Set<String> members(final String group) {
120         Set<String> members = new HashSet<>();
121 
122         MembershipCond membCond = new MembershipCond();
123         membCond.setGroup(group);
124         SearchCond searchCond = SearchCond.getLeaf(membCond);
125         int count = userLogic.search(searchCond, 1, 1, List.of(), SyncopeConstants.ROOT_REALM, true, false).getLeft();
126         for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
127             members.addAll(userLogic.search(
128                     searchCond,
129                     page,
130                     AnyDAO.DEFAULT_PAGE_SIZE,
131                     List.of(),
132                     SyncopeConstants.ROOT_REALM,
133                     true,
134                     false).
135                     getRight().stream().map(UserTO::getKey).collect(Collectors.toSet()));
136         }
137 
138         return members;
139     }
140 
141     @Override
142     public Response update(final String id, final SCIMPatchOp patch) {
143         ResponseBuilder builder = checkETag(Resource.Group, id);
144         if (builder != null) {
145             return builder.build();
146         }
147 
148         patch.getOperations().forEach(op -> {
149             if (op.getPath() != null && "members".equals(op.getPath().getAttribute())) {
150                 if (CollectionUtils.isEmpty(op.getValue())) {
151                     members(id).stream().filter(member -> op.getPath().getFilter() == null
152                             ? true
153                             : BooleanUtils.toBoolean(JexlUtils.evaluate(
154                                     SCIMDataBinder.filter2JexlExpression(op.getPath().getFilter()),
155                                     new MapContext(Map.of("value", member))).toString())).
156                             forEach(member -> changeMembership(member, id, op.getOp()));
157                 } else {
158                     op.getValue().stream().map(Member.class::cast).
159                             forEach(member -> changeMembership(member.getValue(), id, op.getOp()));
160                 }
161             } else {
162                 groupLogic.update(binder.toGroupUR(groupLogic.read(id), op), false);
163             }
164         });
165 
166         return updateResponse(
167                 id,
168                 binder.toSCIMGroup(
169                         groupLogic.read(id),
170                         uriInfo.getAbsolutePathBuilder().path(id).build().toASCIIString(),
171                         List.of(),
172                         List.of()));
173     }
174 
175     @Override
176     public Response replace(final String id, final SCIMGroup group) {
177         if (!id.equals(group.getId())) {
178             throw new BadRequestException(ErrorType.invalidPath, "Expected " + id + ", found " + group.getId());
179         }
180 
181         ResponseBuilder builder = checkETag(Resource.Group, id);
182         if (builder != null) {
183             return builder.build();
184         }
185 
186         // save current group members
187         Set<String> beforeMembers = members(id);
188 
189         // update group, don't change members
190         ProvisioningResult<GroupTO> result = groupLogic.update(
191                 AnyOperations.diff(binder.toGroupTO(group, true), groupLogic.read(id), false), false);
192 
193         // assign new members
194         Set<String> afterMembers = new HashSet<>();
195         group.getMembers().forEach(member -> {
196             afterMembers.add(member.getValue());
197 
198             if (!beforeMembers.contains(member.getValue())) {
199                 changeMembership(member.getValue(), result.getEntity().getKey(), PatchOp.add);
200             }
201         });
202         // remove unconfirmed members
203         beforeMembers.stream().filter(member -> !afterMembers.contains(member)).forEach(user -> changeMembership(
204                 user, result.getEntity().getKey(), PatchOp.remove));
205 
206         return updateResponse(
207                 result.getEntity().getKey(),
208                 binder.toSCIMGroup(
209                         result.getEntity(),
210                         uriInfo.getAbsolutePathBuilder().path(result.getEntity().getKey()).build().toASCIIString(),
211                         List.of(),
212                         List.of()));
213     }
214 
215     @Override
216     public Response delete(final String id) {
217         ResponseBuilder builder = checkETag(Resource.Group, id);
218         if (builder != null) {
219             return builder.build();
220         }
221 
222         anyLogic(Resource.Group).delete(id, false);
223         return Response.noContent().build();
224     }
225 
226     @Override
227     public ListResponse<SCIMGroup> search(
228             final String attributes,
229             final String excludedAttributes,
230             final String filter,
231             final String sortBy,
232             final SortOrder sortOrder,
233             final Integer startIndex,
234             final Integer count) {
235 
236         SCIMSearchRequest request = new SCIMSearchRequest(filter, sortBy, sortOrder, startIndex, count);
237         if (attributes != null) {
238             request.getAttributes().addAll(
239                     List.of(ArrayUtils.nullToEmpty(StringUtils.split(attributes, ','))));
240         }
241         if (excludedAttributes != null) {
242             request.getExcludedAttributes().addAll(
243                     List.of(ArrayUtils.nullToEmpty(StringUtils.split(excludedAttributes, ','))));
244         }
245 
246         return doSearch(Resource.Group, request);
247     }
248 
249     @Override
250     public ListResponse<SCIMGroup> search(final SCIMSearchRequest request) {
251         return doSearch(Resource.Group, request);
252     }
253 }