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.persistence.jpa.dao;
20  
21  import java.time.OffsetDateTime;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  import javax.persistence.NoResultException;
30  import javax.persistence.Query;
31  import javax.persistence.TypedQuery;
32  import org.apache.commons.lang3.tuple.Pair;
33  import org.apache.syncope.common.lib.types.AnyTypeKind;
34  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
35  import org.apache.syncope.core.persistence.api.dao.AnyDAO;
36  import org.apache.syncope.core.persistence.api.dao.AnyMatchDAO;
37  import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
38  import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
39  import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
40  import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
41  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
42  import org.apache.syncope.core.persistence.api.dao.PlainAttrDAO;
43  import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
44  import org.apache.syncope.core.persistence.api.dao.UserDAO;
45  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
46  import org.apache.syncope.core.persistence.api.entity.AnyType;
47  import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
48  import org.apache.syncope.core.persistence.api.entity.AnyUtils;
49  import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
50  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
51  import org.apache.syncope.core.persistence.api.entity.Realm;
52  import org.apache.syncope.core.persistence.api.entity.anyobject.ADynGroupMembership;
53  import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership;
54  import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
55  import org.apache.syncope.core.persistence.api.entity.group.Group;
56  import org.apache.syncope.core.persistence.api.entity.group.TypeExtension;
57  import org.apache.syncope.core.persistence.api.entity.user.UDynGroupMembership;
58  import org.apache.syncope.core.persistence.api.entity.user.UMembership;
59  import org.apache.syncope.core.persistence.api.entity.user.User;
60  import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
61  import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
62  import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAADynGroupMembership;
63  import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAMembership;
64  import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup;
65  import org.apache.syncope.core.persistence.jpa.entity.group.JPATypeExtension;
66  import org.apache.syncope.core.persistence.jpa.entity.user.JPAUDynGroupMembership;
67  import org.apache.syncope.core.persistence.jpa.entity.user.JPAUMembership;
68  import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent;
69  import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
70  import org.apache.syncope.core.spring.security.AuthContextUtils;
71  import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
72  import org.identityconnectors.framework.common.objects.SyncDeltaType;
73  import org.springframework.context.ApplicationEventPublisher;
74  import org.springframework.transaction.annotation.Transactional;
75  
76  public class JPAGroupDAO extends AbstractAnyDAO<Group> implements GroupDAO {
77  
78      public static final String UDYNMEMB_TABLE = "UDynGroupMembers";
79  
80      public static final String ADYNMEMB_TABLE = "ADynGroupMembers";
81  
82      protected final ApplicationEventPublisher publisher;
83  
84      protected final AnyMatchDAO anyMatchDAO;
85  
86      protected final PlainAttrDAO plainAttrDAO;
87  
88      protected final UserDAO userDAO;
89  
90      protected final AnyObjectDAO anyObjectDAO;
91  
92      protected final AnySearchDAO anySearchDAO;
93  
94      protected final SearchCondVisitor searchCondVisitor;
95  
96      public JPAGroupDAO(
97              final AnyUtilsFactory anyUtilsFactory,
98              final ApplicationEventPublisher publisher,
99              final PlainSchemaDAO plainSchemaDAO,
100             final DerSchemaDAO derSchemaDAO,
101             final DynRealmDAO dynRealmDAO,
102             final AnyMatchDAO anyMatchDAO,
103             final PlainAttrDAO plainAttrDAO,
104             final UserDAO userDAO,
105             final AnyObjectDAO anyObjectDAO,
106             final AnySearchDAO searchDAO,
107             final SearchCondVisitor searchCondVisitor) {
108 
109         super(anyUtilsFactory, plainSchemaDAO, derSchemaDAO, dynRealmDAO);
110         this.publisher = publisher;
111         this.anyMatchDAO = anyMatchDAO;
112         this.plainAttrDAO = plainAttrDAO;
113         this.userDAO = userDAO;
114         this.anyObjectDAO = anyObjectDAO;
115         this.anySearchDAO = searchDAO;
116         this.searchCondVisitor = searchCondVisitor;
117     }
118 
119     @Override
120     protected AnyUtils init() {
121         return anyUtilsFactory.getInstance(AnyTypeKind.GROUP);
122     }
123 
124     @Transactional(readOnly = true)
125     @Override
126     public String findKey(final String name) {
127         Query query = entityManager().createNativeQuery("SELECT id FROM " + JPAGroup.TABLE + " WHERE name=?");
128         query.setParameter(1, name);
129 
130         String key = null;
131 
132         for (Object resultKey : query.getResultList()) {
133             key = resultKey instanceof Object[]
134                     ? (String) ((Object[]) resultKey)[0]
135                     : ((String) resultKey);
136         }
137 
138         return key;
139     }
140 
141     @Transactional(readOnly = true)
142     @Override
143     public OffsetDateTime findLastChange(final String key) {
144         return findLastChange(key, JPAGroup.TABLE);
145     }
146 
147     @Override
148     public int count() {
149         Query query = entityManager().createQuery(
150                 "SELECT COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e");
151         return ((Number) query.getSingleResult()).intValue();
152     }
153 
154     @Override
155     public Map<String, Integer> countByRealm() {
156         Query query = entityManager().createQuery(
157                 "SELECT e.realm, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.realm");
158 
159         @SuppressWarnings("unchecked")
160         List<Object[]> results = query.getResultList();
161         return results.stream().collect(Collectors.toMap(
162                 result -> ((Realm) result[0]).getFullPath(),
163                 result -> ((Number) result[1]).intValue()));
164     }
165 
166     @Transactional(readOnly = true)
167     @Override
168     public void securityChecks(
169             final Set<String> authRealms,
170             final String key,
171             final String realm) {
172 
173         // 1. check if AuthContextUtils.getUsername() is owner of the group, or
174         // if group is in Realm (or descendants) for which AuthContextUtils.getUsername() owns entitlement
175         boolean authorized = authRealms.stream().anyMatch(authRealm -> realm.startsWith(authRealm)
176                 || authRealm.equals(RealmUtils.getGroupOwnerRealm(realm, key)));
177 
178         // 2. check if groups is in at least one DynRealm for which AuthContextUtils.getUsername() owns entitlement
179         if (!authorized) {
180             authorized = findDynRealms(key).stream().anyMatch(authRealms::contains);
181         }
182 
183         if (authRealms.isEmpty() || !authorized) {
184             throw new DelegatedAdministrationException(realm, AnyTypeKind.GROUP.name(), key);
185         }
186     }
187 
188     @Override
189     protected void securityChecks(final Group group) {
190         Set<String> authRealms = AuthContextUtils.getAuthorizations().
191                 getOrDefault(IdRepoEntitlement.GROUP_READ, Set.of());
192 
193         securityChecks(authRealms, group.getKey(), group.getRealm().getFullPath());
194     }
195 
196     @Override
197     public Group findByName(final String name) {
198         TypedQuery<Group> query = entityManager().createQuery(
199                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e WHERE e.name = :name", Group.class);
200         query.setParameter("name", name);
201 
202         Group result = null;
203         try {
204             result = query.getSingleResult();
205         } catch (NoResultException e) {
206             LOG.debug("No group found with name {}", name, e);
207         }
208 
209         return result;
210     }
211 
212     @Override
213     public List<String> findKeysByNamePattern(final String pattern) {
214         Query query = entityManager().createNativeQuery(
215                 "SELECT id FROM " + JPAGroup.TABLE + " WHERE LOWER(name) LIKE LOWER(?1)");
216         query.setParameter(1, pattern);
217 
218         @SuppressWarnings("unchecked")
219         List<Object> raw = query.getResultList();
220         return raw.stream().map(Object::toString).collect(Collectors.toList());
221     }
222 
223     @Transactional(readOnly = true)
224     @Override
225     public List<Group> findOwnedByUser(final String userKey) {
226         User owner = userDAO.find(userKey);
227         if (owner == null) {
228             return List.of();
229         }
230 
231         StringBuilder queryString = new StringBuilder("SELECT e FROM ").append(anyUtils().anyClass().getSimpleName())
232                 .append(" e WHERE e.userOwner=:owner ");
233         userDAO.findAllGroupKeys(owner).forEach(groupKey -> queryString
234                 .append("OR e.groupOwner.id='").append(groupKey).append("' "));
235 
236         TypedQuery<Group> query = entityManager().createQuery(queryString.toString(), Group.class);
237         query.setParameter("owner", owner);
238 
239         return query.getResultList();
240     }
241 
242     @Transactional(readOnly = true)
243     @Override
244     public List<Group> findOwnedByGroup(final String groupKey) {
245         Group owner = find(groupKey);
246         if (owner == null) {
247             return List.of();
248         }
249 
250         TypedQuery<Group> query = entityManager().createQuery(
251                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e WHERE e.groupOwner=:owner", Group.class);
252         query.setParameter("owner", owner);
253 
254         return query.getResultList();
255     }
256 
257     @Override
258     public List<AMembership> findAMemberships(final Group group) {
259         TypedQuery<AMembership> query = entityManager().createQuery(
260                 "SELECT e FROM " + JPAAMembership.class.getSimpleName() + " e WHERE e.rightEnd=:group",
261                 AMembership.class);
262         query.setParameter("group", group);
263 
264         return query.getResultList();
265     }
266 
267     @Override
268     public List<UMembership> findUMemberships(final Group group) {
269         TypedQuery<UMembership> query = entityManager().createQuery(
270                 "SELECT e FROM " + JPAUMembership.class.getSimpleName() + " e WHERE e.rightEnd=:group",
271                 UMembership.class);
272         query.setParameter("group", group);
273 
274         return query.getResultList();
275     }
276 
277     @Override
278     public List<Group> findAll(final int page, final int itemsPerPage) {
279         TypedQuery<Group> query = entityManager().createQuery(
280                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", Group.class);
281         query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1));
282         query.setMaxResults(itemsPerPage);
283 
284         return query.getResultList();
285     }
286 
287     @Override
288     public List<String> findAllKeys(final int page, final int itemsPerPage) {
289         return findAllKeys(JPAGroup.TABLE, page, itemsPerPage);
290     }
291 
292     protected SearchCond buildDynMembershipCond(final String baseCondFIQL) {
293         return SearchCondConverter.convert(searchCondVisitor, baseCondFIQL);
294     }
295 
296     @Override
297     public Group saveAndRefreshDynMemberships(final Group group) {
298         Group merged = save(group);
299 
300         // refresh dynamic memberships
301         clearUDynMembers(merged);
302         if (merged.getUDynMembership() != null) {
303             SearchCond cond = buildDynMembershipCond(merged.getUDynMembership().getFIQLCond());
304             int count = anySearchDAO.count(
305                     merged.getRealm(), true, Set.of(merged.getRealm().getFullPath()), cond, AnyTypeKind.USER);
306             for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
307                 List<User> matching = anySearchDAO.search(
308                         merged.getRealm(),
309                         true,
310                         Set.of(merged.getRealm().getFullPath()),
311                         cond,
312                         page,
313                         AnyDAO.DEFAULT_PAGE_SIZE,
314                         List.of(),
315                         AnyTypeKind.USER);
316 
317                 matching.forEach(user -> {
318                     Query insert = entityManager().createNativeQuery(
319                             "INSERT INTO " + UDYNMEMB_TABLE + " VALUES(?, ?)");
320                     insert.setParameter(1, user.getKey());
321                     insert.setParameter(2, merged.getKey());
322                     insert.executeUpdate();
323 
324                     publisher.publishEvent(
325                             new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, user, AuthContextUtils.getDomain()));
326                 });
327             }
328         }
329         clearADynMembers(merged);
330         merged.getADynMemberships().forEach(memb -> {
331             SearchCond cond = buildDynMembershipCond(memb.getFIQLCond());
332             int count = anySearchDAO.count(
333                     merged.getRealm(), true, Set.of(merged.getRealm().getFullPath()), cond, AnyTypeKind.ANY_OBJECT);
334             for (int page = 1; page <= (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
335                 List<AnyObject> matching = anySearchDAO.search(
336                         merged.getRealm(),
337                         true,
338                         Set.of(merged.getRealm().getFullPath()),
339                         cond,
340                         page,
341                         AnyDAO.DEFAULT_PAGE_SIZE,
342                         List.of(),
343                         AnyTypeKind.ANY_OBJECT);
344 
345                 matching.forEach(any -> {
346                     Query insert = entityManager().createNativeQuery(
347                             "INSERT INTO " + ADYNMEMB_TABLE + " VALUES(?, ?, ?)");
348                     insert.setParameter(1, any.getType().getKey());
349                     insert.setParameter(2, any.getKey());
350                     insert.setParameter(3, merged.getKey());
351                     insert.executeUpdate();
352 
353                     publisher.publishEvent(
354                             new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, any, AuthContextUtils.getDomain()));
355                 });
356             }
357         });
358 
359         dynRealmDAO.refreshDynMemberships(merged);
360 
361         return merged;
362     }
363 
364     @Override
365     public void delete(final Group group) {
366         dynRealmDAO.removeDynMemberships(group.getKey());
367 
368         findAMemberships(group).forEach(membership -> {
369             AnyObject leftEnd = membership.getLeftEnd();
370             leftEnd.remove(membership);
371             membership.setRightEnd(null);
372             leftEnd.getPlainAttrs(membership).forEach(attr -> {
373                 leftEnd.remove(attr);
374                 attr.setOwner(null);
375                 attr.setMembership(null);
376                 plainAttrDAO.delete(attr);
377             });
378 
379             anyObjectDAO.save(leftEnd);
380             publisher.publishEvent(
381                     new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, leftEnd, AuthContextUtils.getDomain()));
382         });
383 
384         findUMemberships(group).forEach(membership -> {
385             User leftEnd = membership.getLeftEnd();
386             leftEnd.remove(membership);
387             membership.setRightEnd(null);
388             leftEnd.getPlainAttrs(membership).forEach(attr -> {
389                 leftEnd.remove(attr);
390                 attr.setOwner(null);
391                 attr.setMembership(null);
392 
393                 plainAttrDAO.delete(attr);
394             });
395 
396             userDAO.save(leftEnd);
397             publisher.publishEvent(
398                     new EntityLifecycleEvent<>(this, SyncDeltaType.UPDATE, leftEnd, AuthContextUtils.getDomain()));
399         });
400 
401         clearUDynMembers(group);
402         clearADynMembers(group);
403 
404         entityManager().remove(group);
405     }
406 
407     @Override
408     public List<TypeExtension> findTypeExtensions(final AnyTypeClass anyTypeClass) {
409         TypedQuery<TypeExtension> query = entityManager().createQuery(
410                 "SELECT e FROM " + JPATypeExtension.class.getSimpleName()
411                 + " e WHERE :anyTypeClass MEMBER OF e.auxClasses", TypeExtension.class);
412         query.setParameter("anyTypeClass", anyTypeClass);
413 
414         return query.getResultList();
415     }
416 
417     @Transactional(readOnly = true)
418     @Override
419     public boolean existsAMembership(final String anyObjectKey, final String groupKey) {
420         Query query = entityManager().createNativeQuery(
421                 "SELECT COUNT(*) FROM " + JPAAMembership.TABLE + " WHERE group_id=? AND anyobject_it=?");
422         query.setParameter(1, groupKey);
423         query.setParameter(2, anyObjectKey);
424 
425         return ((Number) query.getSingleResult()).intValue() > 0;
426     }
427 
428     @Transactional(readOnly = true)
429     @Override
430     public boolean existsUMembership(final String userKey, final String groupKey) {
431         Query query = entityManager().createNativeQuery(
432                 "SELECT COUNT(*) FROM " + JPAUMembership.TABLE + " WHERE group_id=? AND user_id=?");
433         query.setParameter(1, groupKey);
434         query.setParameter(2, userKey);
435 
436         return ((Number) query.getSingleResult()).intValue() > 0;
437     }
438 
439     @Transactional(readOnly = true)
440     @Override
441     @SuppressWarnings("unchecked")
442     public List<String> findAMembers(final String groupKey) {
443         Query query = entityManager().createNativeQuery(
444                 "SELECT DISTINCT anyObject_id FROM " + JPAAMembership.TABLE + " WHERE group_id=?");
445         query.setParameter(1, groupKey);
446 
447         List<String> result = new ArrayList<>();
448         query.getResultList().stream().map(key -> key instanceof Object[]
449                 ? (String) ((Object[]) key)[0]
450                 : ((String) key)).
451                 forEach(item -> result.add((String) item));
452         return result;
453     }
454 
455     @Transactional(readOnly = true)
456     @Override
457     @SuppressWarnings("unchecked")
458     public List<String> findUMembers(final String groupKey) {
459         Query query = entityManager().createNativeQuery(
460                 "SELECT DISTINCT user_id FROM " + JPAUMembership.TABLE + " WHERE group_id=?");
461         query.setParameter(1, groupKey);
462 
463         List<String> result = new ArrayList<>();
464         query.getResultList().stream().map(key -> key instanceof Object[]
465                 ? (String) ((Object[]) key)[0]
466                 : ((String) key)).
467                 forEach(item -> result.add((String) item));
468         return result;
469     }
470 
471     @Override
472     @SuppressWarnings("unchecked")
473     public List<String> findADynMembers(final Group group) {
474         List<String> result = new ArrayList<>();
475 
476         group.getADynMemberships().forEach(memb -> {
477             Query query = entityManager().createNativeQuery(
478                     "SELECT DISTINCT any_id FROM " + ADYNMEMB_TABLE + " WHERE group_id=? AND anyType_id=?");
479             query.setParameter(1, group.getKey());
480             query.setParameter(2, memb.getAnyType().getKey());
481 
482             query.getResultList().stream().map(key -> key instanceof Object[]
483                     ? (String) ((Object[]) key)[0]
484                     : ((String) key)).
485                     filter(anyObject -> !result.contains((String) anyObject)).
486                     forEach(anyObject -> result.add((String) anyObject));
487         });
488 
489         return result;
490     }
491 
492     @Override
493     public int countAMembers(final String groupKey) {
494         Query query = entityManager().createNativeQuery(
495                 "SELECT COUNT(DISTINCT anyObject_id) FROM " + JPAAMembership.TABLE + " WHERE group_id=?");
496         query.setParameter(1, groupKey);
497 
498         return ((Number) query.getSingleResult()).intValue();
499     }
500 
501     @Override
502     public int countUMembers(final String groupKey) {
503         Query query = entityManager().createNativeQuery(
504                 "SELECT COUNT(DISTINCT user_id) FROM " + JPAUMembership.TABLE + " WHERE group_id=?");
505         query.setParameter(1, groupKey);
506 
507         return ((Number) query.getSingleResult()).intValue();
508     }
509 
510     @Override
511     public int countADynMembers(final Group group) {
512         Query query = entityManager().createNativeQuery(
513                 "SELECT COUNT(DISTINCT any_id) FROM " + ADYNMEMB_TABLE + " WHERE group_id=?");
514         query.setParameter(1, group.getKey());
515 
516         return ((Number) query.getSingleResult()).intValue();
517     }
518 
519     @Override
520     public int countUDynMembers(final Group group) {
521         if (group.getUDynMembership() == null) {
522             return 0;
523         }
524 
525         Query query = entityManager().createNativeQuery(
526                 "SELECT COUNT(DISTINCT any_id) FROM " + UDYNMEMB_TABLE + " WHERE group_id=?");
527         query.setParameter(1, group.getKey());
528 
529         return ((Number) query.getSingleResult()).intValue();
530     }
531 
532     @Override
533     public void clearADynMembers(final Group group) {
534         Query delete = entityManager().createNativeQuery("DELETE FROM " + ADYNMEMB_TABLE + " WHERE group_id=?");
535         delete.setParameter(1, group.getKey());
536         delete.executeUpdate();
537     }
538 
539     protected List<ADynGroupMembership> findWithADynMemberships(final AnyType anyType) {
540         TypedQuery<ADynGroupMembership> query = entityManager().createQuery(
541                 "SELECT e FROM " + JPAADynGroupMembership.class.getSimpleName() + " e  WHERE e.anyType=:anyType",
542                 ADynGroupMembership.class);
543         query.setParameter("anyType", anyType);
544         return query.getResultList();
545     }
546 
547     @Transactional
548     @Override
549     public Pair<Set<String>, Set<String>> refreshDynMemberships(final AnyObject anyObject) {
550         Set<String> before = new HashSet<>();
551         Set<String> after = new HashSet<>();
552         findWithADynMemberships(anyObject.getType()).forEach(memb -> {
553             boolean matches = anyMatchDAO.matches(anyObject, buildDynMembershipCond(memb.getFIQLCond()));
554             if (matches) {
555                 after.add(memb.getGroup().getKey());
556             }
557 
558             Query query = entityManager().createNativeQuery(
559                     "SELECT COUNT(group_id) FROM " + ADYNMEMB_TABLE + " WHERE group_id=? AND any_id=?");
560             query.setParameter(1, memb.getGroup().getKey());
561             query.setParameter(2, anyObject.getKey());
562             boolean existing = ((Number) query.getSingleResult()).longValue() > 0;
563             if (existing) {
564                 before.add(memb.getGroup().getKey());
565             }
566 
567             if (matches && !existing) {
568                 Query insert = entityManager().createNativeQuery(
569                         "INSERT INTO " + ADYNMEMB_TABLE + " VALUES(?, ?, ?)");
570                 insert.setParameter(1, anyObject.getType().getKey());
571                 insert.setParameter(2, anyObject.getKey());
572                 insert.setParameter(3, memb.getGroup().getKey());
573                 insert.executeUpdate();
574             } else if (!matches && existing) {
575                 Query delete = entityManager().createNativeQuery(
576                         "DELETE FROM " + ADYNMEMB_TABLE + " WHERE group_id=? AND any_id=?");
577                 delete.setParameter(1, memb.getGroup().getKey());
578                 delete.setParameter(2, anyObject.getKey());
579                 delete.executeUpdate();
580             }
581 
582             publisher.publishEvent(new EntityLifecycleEvent<>(
583                     this, SyncDeltaType.UPDATE, memb.getGroup(), AuthContextUtils.getDomain()));
584         });
585 
586         return Pair.of(before, after);
587     }
588 
589     @Override
590     public Set<String> removeDynMemberships(final AnyObject anyObject) {
591         List<Group> dynGroups = anyObjectDAO.findDynGroups(anyObject.getKey());
592 
593         Query delete = entityManager().createNativeQuery("DELETE FROM " + ADYNMEMB_TABLE + " WHERE any_id=?");
594         delete.setParameter(1, anyObject.getKey());
595         delete.executeUpdate();
596 
597         Set<String> before = new HashSet<>();
598         dynGroups.forEach(group -> {
599             before.add(group.getKey());
600 
601             publisher.publishEvent(new EntityLifecycleEvent<>(
602                     this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain()));
603         });
604 
605         return before;
606     }
607 
608     @Override
609     @SuppressWarnings("unchecked")
610     public List<String> findUDynMembers(final Group group) {
611         if (group.getUDynMembership() == null) {
612             return List.of();
613         }
614 
615         Query query = entityManager().createNativeQuery(
616                 "SELECT DISTINCT any_id FROM " + UDYNMEMB_TABLE + " WHERE group_id=?");
617         query.setParameter(1, group.getKey());
618 
619         List<String> result = new ArrayList<>();
620         query.getResultList().stream().map(key -> key instanceof Object[]
621                 ? (String) ((Object[]) key)[0]
622                 : ((String) key)).
623                 forEach(user -> result.add((String) user));
624         return result;
625     }
626 
627     @Override
628     public void clearUDynMembers(final Group group) {
629         Query delete = entityManager().createNativeQuery("DELETE FROM " + UDYNMEMB_TABLE + " WHERE group_id=?");
630         delete.setParameter(1, group.getKey());
631         delete.executeUpdate();
632     }
633 
634     protected List<UDynGroupMembership> findWithUDynMemberships() {
635         TypedQuery<UDynGroupMembership> query = entityManager().createQuery(
636                 "SELECT e FROM " + JPAUDynGroupMembership.class.getSimpleName() + " e",
637                 UDynGroupMembership.class);
638 
639         return query.getResultList();
640     }
641 
642     @Transactional
643     @Override
644     public Pair<Set<String>, Set<String>> refreshDynMemberships(final User user) {
645         Set<String> before = new HashSet<>();
646         Set<String> after = new HashSet<>();
647         findWithUDynMemberships().forEach(memb -> {
648             boolean matches = anyMatchDAO.matches(user, buildDynMembershipCond(memb.getFIQLCond()));
649             if (matches) {
650                 after.add(memb.getGroup().getKey());
651             }
652 
653             Query query = entityManager().createNativeQuery(
654                     "SELECT COUNT(group_id) FROM " + UDYNMEMB_TABLE + " WHERE group_id=? AND any_id=?");
655             query.setParameter(1, memb.getGroup().getKey());
656             query.setParameter(2, user.getKey());
657             boolean existing = ((Number) query.getSingleResult()).longValue() > 0;
658             if (existing) {
659                 before.add(memb.getGroup().getKey());
660             }
661 
662             if (matches && !existing) {
663                 Query insert = entityManager().createNativeQuery(
664                         "INSERT INTO " + UDYNMEMB_TABLE + " VALUES(?, ?)");
665                 insert.setParameter(1, user.getKey());
666                 insert.setParameter(2, memb.getGroup().getKey());
667                 insert.executeUpdate();
668             } else if (!matches && existing) {
669                 Query delete = entityManager().createNativeQuery(
670                         "DELETE FROM " + UDYNMEMB_TABLE + " WHERE group_id=? AND any_id=?");
671                 delete.setParameter(1, memb.getGroup().getKey());
672                 delete.setParameter(2, user.getKey());
673                 delete.executeUpdate();
674             }
675 
676             publisher.publishEvent(new EntityLifecycleEvent<>(
677                     this, SyncDeltaType.UPDATE, memb.getGroup(), AuthContextUtils.getDomain()));
678         });
679 
680         return Pair.of(before, after);
681     }
682 
683     @Override
684     public Set<String> removeDynMemberships(final User user) {
685         List<Group> dynGroups = userDAO.findDynGroups(user.getKey());
686 
687         Query delete = entityManager().createNativeQuery("DELETE FROM " + UDYNMEMB_TABLE + " WHERE any_id=?");
688         delete.setParameter(1, user.getKey());
689         delete.executeUpdate();
690 
691         Set<String> before = new HashSet<>();
692         dynGroups.forEach(group -> {
693             before.add(group.getKey());
694 
695             publisher.publishEvent(new EntityLifecycleEvent<>(
696                     this, SyncDeltaType.UPDATE, group, AuthContextUtils.getDomain()));
697         });
698 
699         return before;
700     }
701 
702     @Transactional(readOnly = true)
703     @Override
704     public Collection<String> findAllResourceKeys(final String key) {
705         return find(key).getResources().stream().map(ExternalResource::getKey).collect(Collectors.toList());
706     }
707 }