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