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.Objects;
28  import java.util.Optional;
29  import java.util.Set;
30  import java.util.stream.Collectors;
31  import javax.persistence.NoResultException;
32  import javax.persistence.Query;
33  import javax.persistence.TypedQuery;
34  import org.apache.commons.lang3.tuple.Pair;
35  import org.apache.syncope.common.lib.types.AnyTypeKind;
36  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
37  import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
38  import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
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.FIQLQueryDAO;
42  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
43  import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
44  import org.apache.syncope.core.persistence.api.dao.RoleDAO;
45  import org.apache.syncope.core.persistence.api.dao.UserDAO;
46  import org.apache.syncope.core.persistence.api.entity.AnyUtils;
47  import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
48  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
49  import org.apache.syncope.core.persistence.api.entity.Membership;
50  import org.apache.syncope.core.persistence.api.entity.Privilege;
51  import org.apache.syncope.core.persistence.api.entity.Realm;
52  import org.apache.syncope.core.persistence.api.entity.Role;
53  import org.apache.syncope.core.persistence.api.entity.group.Group;
54  import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
55  import org.apache.syncope.core.persistence.api.entity.user.SecurityQuestion;
56  import org.apache.syncope.core.persistence.api.entity.user.UMembership;
57  import org.apache.syncope.core.persistence.api.entity.user.User;
58  import org.apache.syncope.core.persistence.jpa.entity.user.JPALinkedAccount;
59  import org.apache.syncope.core.persistence.jpa.entity.user.JPAUMembership;
60  import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
61  import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
62  import org.apache.syncope.core.spring.security.AuthContextUtils;
63  import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
64  import org.apache.syncope.core.spring.security.SecurityProperties;
65  import org.springframework.transaction.annotation.Propagation;
66  import org.springframework.transaction.annotation.Transactional;
67  
68  public class JPAUserDAO extends AbstractAnyDAO<User> implements UserDAO {
69  
70      protected final RoleDAO roleDAO;
71  
72      protected final AccessTokenDAO accessTokenDAO;
73  
74      protected final GroupDAO groupDAO;
75  
76      protected final DelegationDAO delegationDAO;
77  
78      protected final FIQLQueryDAO fiqlQueryDAO;
79  
80      protected final SecurityProperties securityProperties;
81  
82      public JPAUserDAO(
83              final AnyUtilsFactory anyUtilsFactory,
84              final PlainSchemaDAO plainSchemaDAO,
85              final DerSchemaDAO derSchemaDAO,
86              final DynRealmDAO dynRealmDAO,
87              final RoleDAO roleDAO,
88              final AccessTokenDAO accessTokenDAO,
89              final GroupDAO groupDAO,
90              final DelegationDAO delegationDAO,
91              final FIQLQueryDAO fiqlQueryDAO,
92              final SecurityProperties securityProperties) {
93  
94          super(anyUtilsFactory, plainSchemaDAO, derSchemaDAO, dynRealmDAO);
95          this.roleDAO = roleDAO;
96          this.accessTokenDAO = accessTokenDAO;
97          this.groupDAO = groupDAO;
98          this.delegationDAO = delegationDAO;
99          this.fiqlQueryDAO = fiqlQueryDAO;
100         this.securityProperties = securityProperties;
101     }
102 
103     @Override
104     protected AnyUtils init() {
105         return anyUtilsFactory.getInstance(AnyTypeKind.USER);
106     }
107 
108     @Transactional(readOnly = true)
109     @Override
110     public String findKey(final String username) {
111         Query query = entityManager().createNativeQuery("SELECT id FROM " + JPAUser.TABLE + " WHERE username=?");
112         query.setParameter(1, username);
113 
114         String key = null;
115 
116         for (Object resultKey : query.getResultList()) {
117             key = resultKey instanceof Object[]
118                     ? (String) ((Object[]) resultKey)[0]
119                     : ((String) resultKey);
120         }
121 
122         return key;
123     }
124 
125     @Transactional(readOnly = true)
126     @Override
127     public OffsetDateTime findLastChange(final String key) {
128         return findLastChange(key, JPAUser.TABLE);
129     }
130 
131     @Transactional(readOnly = true)
132     @Override
133     public Optional<String> findUsername(final String key) {
134         Query query = entityManager().createNativeQuery("SELECT username FROM " + JPAUser.TABLE + " WHERE id=?");
135         query.setParameter(1, key);
136 
137         String username = null;
138         for (Object resultKey : query.getResultList()) {
139             username = resultKey instanceof Object[]
140                     ? (String) ((Object[]) resultKey)[0]
141                     : ((String) resultKey);
142         }
143 
144         return Optional.ofNullable(username);
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     @Override
167     public Map<String, Integer> countByStatus() {
168         Query query = entityManager().createQuery(
169                 "SELECT e.status, COUNT(e) FROM " + anyUtils().anyClass().getSimpleName() + " e GROUP BY e.status");
170 
171         @SuppressWarnings("unchecked")
172         List<Object[]> results = query.getResultList();
173         return results.stream().collect(Collectors.toMap(
174                 result -> (String) result[0],
175                 result -> ((Number) result[1]).intValue()));
176     }
177 
178     @Transactional(readOnly = true)
179     @Override
180     public void securityChecks(
181             final Set<String> authRealms,
182             final String key,
183             final String realm,
184             final Collection<String> groups) {
185 
186         // 1. check if AuthContextUtils.getUsername() is owner of at least one group of which user is member
187         boolean authorized = authRealms.stream().
188                 map(authRealm -> RealmUtils.parseGroupOwnerRealm(authRealm).orElse(null)).
189                 filter(Objects::nonNull).
190                 anyMatch(pair -> groups.contains(pair.getRight()));
191 
192         // 2. check if user is in at least one DynRealm for which AuthContextUtils.getUsername() owns entitlement
193         if (!authorized) {
194             authorized = findDynRealms(key).stream().anyMatch(authRealms::contains);
195         }
196 
197         // 3. check if user is in Realm (or descendants) for which AuthContextUtils.getUsername() owns entitlement
198         if (!authorized) {
199             authorized = authRealms.stream().anyMatch(realm::startsWith);
200         }
201 
202         if (!authorized) {
203             throw new DelegatedAdministrationException(realm, AnyTypeKind.USER.name(), key);
204         }
205     }
206 
207     @Override
208     protected void securityChecks(final User user) {
209         // Allows anonymous (during self-registration) and self (during self-update) to read own user,
210         // otherwise goes through security checks to see if required entitlements are owned
211         if (!AuthContextUtils.getUsername().equals(securityProperties.getAnonymousUser())
212                 && !AuthContextUtils.getUsername().equals(user.getUsername())) {
213 
214             Set<String> authRealms = AuthContextUtils.getAuthorizations().
215                     getOrDefault(IdRepoEntitlement.USER_READ, Set.of());
216 
217             securityChecks(authRealms, user.getKey(), user.getRealm().getFullPath(), findAllGroupKeys(user));
218         }
219     }
220 
221     @Override
222     public User findByUsername(final String username) {
223         TypedQuery<User> query = entityManager().createQuery(
224                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName()
225                 + " e WHERE e.username = :username", User.class);
226         query.setParameter("username", username);
227 
228         User result = null;
229         try {
230             result = query.getSingleResult();
231         } catch (NoResultException e) {
232             LOG.debug("No user found with username {}", username, e);
233         }
234 
235         return result;
236     }
237 
238     @Override
239     public User findByToken(final String token) {
240         TypedQuery<User> query = entityManager().createQuery(
241                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName()
242                 + " e WHERE e.token LIKE :token", User.class);
243         query.setParameter("token", token);
244 
245         User result = null;
246         try {
247             result = query.getSingleResult();
248         } catch (NoResultException e) {
249             LOG.debug("No user found with token {}", token, e);
250         }
251 
252         return result;
253     }
254 
255     @Override
256     public List<User> findBySecurityQuestion(final SecurityQuestion securityQuestion) {
257         TypedQuery<User> query = entityManager().createQuery(
258                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName()
259                 + " e WHERE e.securityQuestion = :securityQuestion", User.class);
260         query.setParameter("securityQuestion", securityQuestion);
261 
262         return query.getResultList();
263     }
264 
265     @Override
266     public UMembership findMembership(final String key) {
267         return entityManager().find(JPAUMembership.class, key);
268     }
269 
270     @Override
271     public List<User> findAll(final int page, final int itemsPerPage) {
272         TypedQuery<User> query = entityManager().createQuery(
273                 "SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e ORDER BY e.id", User.class);
274         query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1));
275         query.setMaxResults(itemsPerPage);
276 
277         return query.getResultList();
278     }
279 
280     @Override
281     public List<String> findAllKeys(final int page, final int itemsPerPage) {
282         return findAllKeys(JPAUser.TABLE, page, itemsPerPage);
283     }
284 
285     protected Pair<User, Pair<Set<String>, Set<String>>> doSave(final User user) {
286         User merged = super.save(user);
287         roleDAO.refreshDynMemberships(merged);
288         Pair<Set<String>, Set<String>> dynGroupMembs = groupDAO.refreshDynMemberships(merged);
289         dynRealmDAO.refreshDynMemberships(merged);
290 
291         return Pair.of(merged, dynGroupMembs);
292     }
293 
294     @Override
295     public User save(final User user) {
296         return doSave(user).getLeft();
297     }
298 
299     @Override
300     public Pair<Set<String>, Set<String>> saveAndGetDynGroupMembs(final User user) {
301         return doSave(user).getRight();
302     }
303 
304     @Override
305     public void delete(final User user) {
306         roleDAO.removeDynMemberships(user.getKey());
307         groupDAO.removeDynMemberships(user);
308         dynRealmDAO.removeDynMemberships(user.getKey());
309 
310         delegationDAO.findByDelegating(user).forEach(delegationDAO::delete);
311         delegationDAO.findByDelegated(user).forEach(delegationDAO::delete);
312 
313         fiqlQueryDAO.findByOwner(user, null).forEach(fiqlQueryDAO::delete);
314 
315         Optional.ofNullable(accessTokenDAO.findByOwner(user.getUsername())).ifPresent(accessTokenDAO::delete);
316 
317         entityManager().remove(user);
318     }
319 
320     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
321     @Override
322     public Collection<Role> findAllRoles(final User user) {
323         Set<Role> result = new HashSet<>();
324         result.addAll(user.getRoles());
325         result.addAll(findDynRoles(user.getKey()));
326 
327         return result;
328     }
329 
330     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
331     @Override
332     @SuppressWarnings("unchecked")
333     public List<Role> findDynRoles(final String key) {
334         Query query = entityManager().createNativeQuery(
335                 "SELECT role_id FROM " + JPARoleDAO.DYNMEMB_TABLE + " WHERE any_id=?");
336         query.setParameter(1, key);
337 
338         List<Role> result = new ArrayList<>();
339         query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
340                 ? (String) ((Object[]) resultKey)[0]
341                 : ((String) resultKey)).
342                 forEach(roleKey -> {
343                     Role role = roleDAO.find(roleKey.toString());
344                     if (role == null) {
345                         LOG.error("Could not find role {}, even though returned by the native query", roleKey);
346                     } else if (!result.contains(role)) {
347                         result.add(role);
348                     }
349                 });
350         return result;
351     }
352 
353     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
354     @Override
355     @SuppressWarnings("unchecked")
356     public List<Group> findDynGroups(final String key) {
357         Query query = entityManager().createNativeQuery(
358                 "SELECT group_id FROM " + JPAGroupDAO.UDYNMEMB_TABLE + " WHERE any_id=?");
359         query.setParameter(1, key);
360 
361         List<Group> result = new ArrayList<>();
362         query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
363                 ? (String) ((Object[]) resultKey)[0]
364                 : ((String) resultKey)).
365                 forEach(groupKey -> {
366                     Group group = groupDAO.find(groupKey.toString());
367                     if (group == null) {
368                         LOG.error("Could not find group {}, even though returned by the native query", groupKey);
369                     } else if (!result.contains(group)) {
370                         result.add(group);
371                     }
372                 });
373         return result;
374     }
375 
376     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
377     @Override
378     public Collection<Group> findAllGroups(final User user) {
379         Set<Group> result = new HashSet<>();
380         result.addAll(user.getMemberships().stream().
381                 map(Membership::getRightEnd).collect(Collectors.toSet()));
382         result.addAll(findDynGroups(user.getKey()));
383 
384         return result;
385     }
386 
387     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
388     @Override
389     public Collection<String> findAllGroupKeys(final User user) {
390         return findAllGroups(user).stream().map(Group::getKey).collect(Collectors.toList());
391     }
392 
393     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
394     @Override
395     public Collection<String> findAllGroupNames(final User user) {
396         return findAllGroups(user).stream().map(Group::getName).collect(Collectors.toList());
397     }
398 
399     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
400     @Override
401     public Collection<ExternalResource> findAllResources(final User user) {
402         Set<ExternalResource> result = new HashSet<>();
403         result.addAll(user.getResources());
404         findAllGroups(user).forEach(group -> result.addAll(group.getResources()));
405 
406         return result;
407     }
408 
409     @Transactional(readOnly = true)
410     @Override
411     public Collection<String> findAllResourceKeys(final String key) {
412         return findAllResources(authFind(key)).stream().map(ExternalResource::getKey).collect(Collectors.toList());
413     }
414 
415     @Transactional(readOnly = true)
416     @Override
417     public boolean linkedAccountExists(final String userKey, final String connObjectKeyValue) {
418         Query query = entityManager().createNativeQuery(
419                 "SELECT id FROM " + JPALinkedAccount.TABLE + " WHERE owner_id=? AND connObjectKeyValue=?");
420         query.setParameter(1, userKey);
421         query.setParameter(2, connObjectKeyValue);
422 
423         return !query.getResultList().isEmpty();
424     }
425 
426     @Override
427     public Optional<? extends LinkedAccount> findLinkedAccount(
428             final ExternalResource resource, final String connObjectKeyValue) {
429 
430         TypedQuery<LinkedAccount> query = entityManager().createQuery(
431                 "SELECT e FROM " + JPALinkedAccount.class.getSimpleName() + " e "
432                 + "WHERE e.resource=:resource AND e.connObjectKeyValue=:connObjectKeyValue", LinkedAccount.class);
433         query.setParameter("resource", resource);
434         query.setParameter("connObjectKeyValue", connObjectKeyValue);
435 
436         List<LinkedAccount> result = query.getResultList();
437         return result.isEmpty() ? Optional.empty() : Optional.of(result.get(0));
438     }
439 
440     @Transactional(readOnly = true)
441     @Override
442     public List<LinkedAccount> findLinkedAccounts(final String userKey) {
443         TypedQuery<LinkedAccount> query = entityManager().createQuery(
444                 "SELECT e FROM " + JPALinkedAccount.class.getSimpleName() + " e "
445                 + "WHERE e.owner.id=:userKey", LinkedAccount.class);
446         query.setParameter("userKey", userKey);
447         return query.getResultList();
448     }
449 
450     @Override
451     public List<LinkedAccount> findLinkedAccountsByPrivilege(final Privilege privilege) {
452         TypedQuery<LinkedAccount> query = entityManager().createQuery(
453                 "SELECT e FROM " + JPALinkedAccount.class.getSimpleName() + " e "
454                 + "WHERE :privilege MEMBER OF e.privileges", LinkedAccount.class);
455         query.setParameter("privilege", privilege);
456         return query.getResultList();
457     }
458 
459     @Override
460     public List<LinkedAccount> findLinkedAccountsByResource(final ExternalResource resource) {
461         TypedQuery<LinkedAccount> query = entityManager().createQuery(
462                 "SELECT e FROM " + JPALinkedAccount.class.getSimpleName() + " e "
463                 + "WHERE e.resource=:resource", LinkedAccount.class);
464         query.setParameter("resource", resource);
465 
466         return query.getResultList();
467     }
468 }