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.logic;
20  
21  import java.lang.reflect.Method;
22  import java.time.OffsetDateTime;
23  import java.util.Collection;
24  import java.util.Comparator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  import org.apache.commons.lang3.ArrayUtils;
31  import org.apache.commons.lang3.tuple.Pair;
32  import org.apache.syncope.common.lib.SyncopeClientException;
33  import org.apache.syncope.common.lib.request.GroupCR;
34  import org.apache.syncope.common.lib.request.GroupUR;
35  import org.apache.syncope.common.lib.request.StringPatchItem;
36  import org.apache.syncope.common.lib.to.ExecTO;
37  import org.apache.syncope.common.lib.to.GroupTO;
38  import org.apache.syncope.common.lib.to.PropagationStatus;
39  import org.apache.syncope.common.lib.to.ProvisioningResult;
40  import org.apache.syncope.common.lib.types.AnyTypeKind;
41  import org.apache.syncope.common.lib.types.ClientExceptionType;
42  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
43  import org.apache.syncope.common.lib.types.IdRepoImplementationType;
44  import org.apache.syncope.common.lib.types.ImplementationEngine;
45  import org.apache.syncope.common.lib.types.JobType;
46  import org.apache.syncope.common.lib.types.PatchOperation;
47  import org.apache.syncope.common.lib.types.ProvisionAction;
48  import org.apache.syncope.common.lib.types.TaskType;
49  import org.apache.syncope.core.logic.api.LogicActions;
50  import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
51  import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
52  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
53  import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
54  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
55  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
56  import org.apache.syncope.core.persistence.api.dao.TaskDAO;
57  import org.apache.syncope.core.persistence.api.dao.UserDAO;
58  import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
59  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
60  import org.apache.syncope.core.persistence.api.entity.EntityFactory;
61  import org.apache.syncope.core.persistence.api.entity.Implementation;
62  import org.apache.syncope.core.persistence.api.entity.Realm;
63  import org.apache.syncope.core.persistence.api.entity.group.Group;
64  import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
65  import org.apache.syncope.core.provisioning.api.GroupProvisioningManager;
66  import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
67  import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
68  import org.apache.syncope.core.provisioning.api.job.JobManager;
69  import org.apache.syncope.core.provisioning.api.job.JobNamer;
70  import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
71  import org.apache.syncope.core.provisioning.java.job.GroupMemberProvisionTaskJobDelegate;
72  import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
73  import org.apache.syncope.core.spring.security.AuthContextUtils;
74  import org.apache.syncope.core.spring.security.SecurityProperties;
75  import org.quartz.JobDataMap;
76  import org.springframework.scheduling.quartz.SchedulerFactoryBean;
77  import org.springframework.security.access.prepost.PreAuthorize;
78  import org.springframework.transaction.annotation.Transactional;
79  
80  /**
81   * Note that this controller does not extend {@link AbstractTransactionalLogic}, hence does not provide any
82   * Spring's Transactional logic at class level.
83   */
84  public class GroupLogic extends AbstractAnyLogic<GroupTO, GroupCR, GroupUR> {
85  
86      protected final UserDAO userDAO;
87  
88      protected final GroupDAO groupDAO;
89  
90      protected final SecurityProperties securityProperties;
91  
92      protected final AnySearchDAO searchDAO;
93  
94      protected final ImplementationDAO implementationDAO;
95  
96      protected final TaskDAO taskDAO;
97  
98      protected final GroupDataBinder binder;
99  
100     protected final GroupProvisioningManager provisioningManager;
101 
102     protected final TaskDataBinder taskDataBinder;
103 
104     protected final JobManager jobManager;
105 
106     protected final SchedulerFactoryBean scheduler;
107 
108     protected final EntityFactory entityFactory;
109 
110     public GroupLogic(
111             final RealmDAO realmDAO,
112             final AnyTypeDAO anyTypeDAO,
113             final TemplateUtils templateUtils,
114             final UserDAO userDAO,
115             final GroupDAO groupDAO,
116             final SecurityProperties securityProperties,
117             final AnySearchDAO searchDAO,
118             final ImplementationDAO implementationDAO,
119             final TaskDAO taskDAO,
120             final GroupDataBinder binder,
121             final GroupProvisioningManager provisioningManager,
122             final TaskDataBinder taskDataBinder,
123             final JobManager jobManager,
124             final SchedulerFactoryBean scheduler,
125             final EntityFactory entityFactory) {
126 
127         super(realmDAO, anyTypeDAO, templateUtils);
128 
129         this.userDAO = userDAO;
130         this.groupDAO = groupDAO;
131         this.securityProperties = securityProperties;
132         this.searchDAO = searchDAO;
133         this.implementationDAO = implementationDAO;
134         this.taskDAO = taskDAO;
135         this.binder = binder;
136         this.provisioningManager = provisioningManager;
137         this.taskDataBinder = taskDataBinder;
138         this.jobManager = jobManager;
139         this.scheduler = scheduler;
140         this.entityFactory = entityFactory;
141     }
142 
143     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_READ + "')")
144     @Transactional(readOnly = true)
145     @Override
146     public GroupTO read(final String key) {
147         return binder.getGroupTO(key);
148     }
149 
150     @PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "'))")
151     @Transactional(readOnly = true)
152     public List<GroupTO> own() {
153         if (securityProperties.getAdminUser().equals(AuthContextUtils.getUsername())) {
154             return List.of();
155         }
156 
157         return userDAO.findAllGroups(userDAO.findByUsername(AuthContextUtils.getUsername())).stream().
158                 map(group -> binder.getGroupTO(group, true)).collect(Collectors.toList());
159     }
160 
161     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_SEARCH + "')")
162     @Transactional(readOnly = true)
163     @Override
164     public Pair<Integer, List<GroupTO>> search(
165             final SearchCond searchCond,
166             final int page, final int size, final List<OrderByClause> orderBy,
167             final String realm,
168             final boolean recursive,
169             final boolean details) {
170 
171         Realm base = Optional.ofNullable(realmDAO.findByFullPath(realm)).
172                 orElseThrow(() -> new NotFoundException("Realm " + realm));
173 
174         Set<String> authRealms = RealmUtils.getEffective(
175                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_SEARCH), realm);
176 
177         SearchCond effectiveCond = searchCond == null ? groupDAO.getAllMatchingCond() : searchCond;
178 
179         int count = searchDAO.count(base, recursive, authRealms, effectiveCond, AnyTypeKind.GROUP);
180 
181         List<Group> matching = searchDAO.search(
182                 base, recursive, authRealms, effectiveCond, page, size, orderBy, AnyTypeKind.GROUP);
183         List<GroupTO> result = matching.stream().
184                 map(group -> binder.getGroupTO(group, details)).
185                 collect(Collectors.toList());
186 
187         return Pair.of(count, result);
188     }
189 
190     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_CREATE + "')")
191     public ProvisioningResult<GroupTO> create(final GroupCR createReq, final boolean nullPriorityAsync) {
192         Pair<GroupCR, List<LogicActions>> before = beforeCreate(createReq);
193 
194         if (before.getLeft().getRealm() == null) {
195             throw SyncopeClientException.build(ClientExceptionType.InvalidRealm);
196         }
197 
198         Set<String> authRealms = RealmUtils.getEffective(
199                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_CREATE),
200                 before.getLeft().getRealm());
201         groupDAO.securityChecks(
202                 authRealms,
203                 null,
204                 before.getLeft().getRealm());
205 
206         Pair<String, List<PropagationStatus>> created = provisioningManager.create(
207                 before.getLeft(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
208 
209         return afterCreate(binder.getGroupTO(created.getKey()), created.getRight(), before.getRight());
210     }
211 
212     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
213     @Override
214     public ProvisioningResult<GroupTO> update(final GroupUR req, final boolean nullPriorityAsync) {
215         GroupTO groupTO = binder.getGroupTO(req.getKey());
216         Pair<GroupUR, List<LogicActions>> before = beforeUpdate(req, groupTO.getRealm());
217 
218         Set<String> authRealms = RealmUtils.getEffective(
219                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_UPDATE),
220                 groupTO.getRealm());
221         groupDAO.securityChecks(
222                 authRealms,
223                 before.getLeft().getKey(),
224                 groupTO.getRealm());
225 
226         Pair<GroupUR, List<PropagationStatus>> after = provisioningManager.update(
227                 req, Set.of(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
228 
229         ProvisioningResult<GroupTO> result = afterUpdate(
230                 binder.getGroupTO(after.getLeft().getKey()),
231                 after.getRight(),
232                 before.getRight());
233 
234         // check if group can still be managed by the caller
235         authRealms = RealmUtils.getEffective(
236                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_UPDATE),
237                 result.getEntity().getRealm());
238         groupDAO.securityChecks(
239                 authRealms,
240                 after.getLeft().getKey(),
241                 result.getEntity().getRealm());
242 
243         return result;
244     }
245 
246     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_DELETE + "')")
247     @Override
248     public ProvisioningResult<GroupTO> delete(final String key, final boolean nullPriorityAsync) {
249         GroupTO group = binder.getGroupTO(key);
250         Pair<GroupTO, List<LogicActions>> before = beforeDelete(group);
251 
252         Set<String> authRealms = RealmUtils.getEffective(
253                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_DELETE),
254                 before.getLeft().getRealm());
255         groupDAO.securityChecks(
256                 authRealms,
257                 before.getLeft().getKey(),
258                 before.getLeft().getRealm());
259 
260         List<Group> ownedGroups = groupDAO.findOwnedByGroup(before.getLeft().getKey());
261         if (!ownedGroups.isEmpty()) {
262             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.GroupOwnership);
263             sce.getElements().addAll(ownedGroups.stream().
264                     map(g -> g.getKey() + ' ' + g.getName()).collect(Collectors.toList()));
265             throw sce;
266         }
267 
268         List<PropagationStatus> statuses = provisioningManager.delete(
269                 before.getLeft().getKey(), nullPriorityAsync, AuthContextUtils.getUsername(), REST_CONTEXT);
270 
271         GroupTO groupTO = new GroupTO();
272         groupTO.setKey(before.getLeft().getKey());
273 
274         return afterDelete(groupTO, statuses, before.getRight());
275     }
276 
277     protected GroupTO updateChecks(final String key) {
278         GroupTO group = binder.getGroupTO(key);
279 
280         Set<String> authRealms = RealmUtils.getEffective(
281                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_UPDATE),
282                 group.getRealm());
283         groupDAO.securityChecks(
284                 authRealms,
285                 group.getKey(),
286                 group.getRealm());
287 
288         return group;
289     }
290 
291     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
292     @Override
293     public GroupTO unlink(final String key, final Collection<String> resources) {
294         GroupTO groupTO = updateChecks(key);
295 
296         GroupUR req = new GroupUR.Builder(key).
297                 resources(resources.stream().
298                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.DELETE).value(r).build()).
299                         collect(Collectors.toList())).
300                 udynMembershipCond(groupTO.getUDynMembershipCond()).
301                 adynMembershipConds(groupTO.getADynMembershipConds()).
302                 build();
303 
304         return binder.getGroupTO(provisioningManager.unlink(req, AuthContextUtils.getUsername(), REST_CONTEXT));
305     }
306 
307     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
308     @Override
309     public GroupTO link(final String key, final Collection<String> resources) {
310         GroupTO groupTO = updateChecks(key);
311 
312         GroupUR req = new GroupUR.Builder(key).
313                 resources(resources.stream().
314                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.ADD_REPLACE).value(r).build()).
315                         collect(Collectors.toList())).
316                 udynMembershipCond(groupTO.getUDynMembershipCond()).
317                 adynMembershipConds(groupTO.getADynMembershipConds()).
318                 build();
319 
320         return binder.getGroupTO(provisioningManager.link(req, AuthContextUtils.getUsername(), REST_CONTEXT));
321     }
322 
323     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
324     @Override
325     public ProvisioningResult<GroupTO> unassign(
326             final String key, final Collection<String> resources, final boolean nullPriorityAsync) {
327 
328         GroupTO groupTO = updateChecks(key);
329 
330         GroupUR req = new GroupUR.Builder(key).
331                 resources(resources.stream().
332                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.DELETE).value(r).build()).
333                         collect(Collectors.toList())).
334                 udynMembershipCond(groupTO.getUDynMembershipCond()).
335                 adynMembershipConds(groupTO.getADynMembershipConds()).
336                 build();
337 
338         return update(req, nullPriorityAsync);
339     }
340 
341     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
342     @Override
343     public ProvisioningResult<GroupTO> assign(
344             final String key,
345             final Collection<String> resources,
346             final boolean changepwd,
347             final String password,
348             final boolean nullPriorityAsync) {
349 
350         GroupTO groupTO = updateChecks(key);
351 
352         GroupUR req = new GroupUR.Builder(key).
353                 resources(resources.stream().
354                         map(r -> new StringPatchItem.Builder().operation(PatchOperation.ADD_REPLACE).value(r).build()).
355                         collect(Collectors.toList())).
356                 udynMembershipCond(groupTO.getUDynMembershipCond()).
357                 adynMembershipConds(groupTO.getADynMembershipConds()).
358                 build();
359 
360         return update(req, nullPriorityAsync);
361     }
362 
363     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
364     @Override
365     public ProvisioningResult<GroupTO> deprovision(
366             final String key,
367             final List<String> resources,
368             final boolean nullPriorityAsync) {
369 
370         updateChecks(key);
371 
372         List<PropagationStatus> statuses = provisioningManager.deprovision(
373                 key, resources, nullPriorityAsync, AuthContextUtils.getUsername());
374 
375         ProvisioningResult<GroupTO> result = new ProvisioningResult<>();
376         result.setEntity(binder.getGroupTO(key));
377         result.getPropagationStatuses().addAll(statuses);
378         result.getPropagationStatuses().sort(Comparator.comparing(item -> resources.indexOf(item.getResource())));
379         return result;
380     }
381 
382     @PreAuthorize("hasRole('" + IdRepoEntitlement.GROUP_UPDATE + "')")
383     @Override
384     public ProvisioningResult<GroupTO> provision(
385             final String key,
386             final List<String> resources,
387             final boolean changePwd,
388             final String password,
389             final boolean nullPriorityAsync) {
390 
391         updateChecks(key);
392 
393         List<PropagationStatus> statuses = provisioningManager.provision(
394                 key, resources, nullPriorityAsync, AuthContextUtils.getUsername());
395 
396         ProvisioningResult<GroupTO> result = new ProvisioningResult<>();
397         result.setEntity(binder.getGroupTO(key));
398         result.getPropagationStatuses().addAll(statuses);
399         result.getPropagationStatuses().sort(Comparator.comparing(item -> resources.indexOf(item.getResource())));
400         return result;
401     }
402 
403     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_CREATE + "') "
404             + "and hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
405     @Transactional
406     public ExecTO provisionMembers(final String key, final ProvisionAction action) {
407         Group group = groupDAO.find(key);
408         if (group == null) {
409             throw new NotFoundException("Group " + key);
410         }
411 
412         Implementation jobDelegate = implementationDAO.findByType(IdRepoImplementationType.TASKJOB_DELEGATE).stream().
413                 filter(impl -> GroupMemberProvisionTaskJobDelegate.class.getName().equals(impl.getBody())).
414                 findFirst().orElseGet(() -> {
415                     Implementation groupMemberProvision = entityFactory.newEntity(Implementation.class);
416                     groupMemberProvision.setKey(GroupMemberProvisionTaskJobDelegate.class.getSimpleName());
417                     groupMemberProvision.setEngine(ImplementationEngine.JAVA);
418                     groupMemberProvision.setType(IdRepoImplementationType.TASKJOB_DELEGATE);
419                     groupMemberProvision.setBody(GroupMemberProvisionTaskJobDelegate.class.getName());
420                     groupMemberProvision = implementationDAO.save(groupMemberProvision);
421                     return groupMemberProvision;
422                 });
423 
424         String name = (action == ProvisionAction.DEPROVISION ? "de" : "")
425                 + "provision members of group " + group.getName();
426         SchedTask task = taskDAO.<SchedTask>findByName(TaskType.SCHEDULED, name).
427                 orElseGet(() -> {
428                     SchedTask t = entityFactory.newEntity(SchedTask.class);
429                     t.setName(name);
430                     return t;
431                 });
432         task.setActive(true);
433         task.setJobDelegate(jobDelegate);
434         task = taskDAO.save(task);
435 
436         try {
437             Map<String, Object> jobDataMap = jobManager.register(
438                     task,
439                     null,
440                     AuthContextUtils.getUsername());
441 
442             jobDataMap.put(JobManager.DRY_RUN_JOBDETAIL_KEY, false);
443             jobDataMap.put(GroupMemberProvisionTaskJobDelegate.GROUP_KEY_JOBDETAIL_KEY, key);
444             jobDataMap.put(GroupMemberProvisionTaskJobDelegate.ACTION_JOBDETAIL_KEY, action);
445 
446             scheduler.getScheduler().triggerJob(
447                     JobNamer.getJobKey(task),
448                     new JobDataMap(jobDataMap));
449         } catch (Exception e) {
450             LOG.error("While executing task {}", task, e);
451 
452             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
453             sce.getElements().add(e.getMessage());
454             throw sce;
455         }
456 
457         ExecTO result = new ExecTO();
458         result.setJobType(JobType.TASK);
459         result.setRefKey(task.getKey());
460         result.setRefDesc(taskDataBinder.buildRefDesc(task));
461         result.setStart(OffsetDateTime.now());
462         result.setStatus("JOB_FIRED");
463         result.setMessage("Job fired; waiting for results...");
464 
465         return result;
466     }
467 
468     @Override
469     protected GroupTO resolveReference(final Method method, final Object... args) throws UnresolvedReferenceException {
470         String key = null;
471 
472         if (ArrayUtils.isNotEmpty(args)) {
473             for (int i = 0; key == null && i < args.length; i++) {
474                 if (args[i] instanceof String) {
475                     key = (String) args[i];
476                 } else if (args[i] instanceof GroupTO) {
477                     key = ((GroupTO) args[i]).getKey();
478                 } else if (args[i] instanceof GroupUR) {
479                     key = ((GroupUR) args[i]).getKey();
480                 }
481             }
482         }
483 
484         if (key != null) {
485             try {
486                 return binder.getGroupTO(key);
487             } catch (Throwable ignore) {
488                 LOG.debug("Unresolved reference", ignore);
489                 throw new UnresolvedReferenceException(ignore);
490             }
491         }
492 
493         throw new UnresolvedReferenceException();
494     }
495 }