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.spring.security;
20  
21  import java.time.OffsetDateTime;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.Iterator;
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 javax.security.auth.login.AccountNotFoundException;
31  import org.apache.commons.lang3.ArrayUtils;
32  import org.apache.commons.lang3.BooleanUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.lang3.tuple.Pair;
35  import org.apache.commons.lang3.tuple.Triple;
36  import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
37  import org.apache.syncope.common.lib.SyncopeConstants;
38  import org.apache.syncope.common.lib.to.Provision;
39  import org.apache.syncope.common.lib.types.AnyTypeKind;
40  import org.apache.syncope.common.lib.types.AuditElements;
41  import org.apache.syncope.common.lib.types.EntitlementsHolder;
42  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
43  import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
44  import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
45  import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
46  import org.apache.syncope.core.persistence.api.dao.GroupDAO;
47  import org.apache.syncope.core.persistence.api.dao.RealmDAO;
48  import org.apache.syncope.core.persistence.api.dao.RoleDAO;
49  import org.apache.syncope.core.persistence.api.dao.UserDAO;
50  import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
51  import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
52  import org.apache.syncope.core.persistence.api.entity.AccessToken;
53  import org.apache.syncope.core.persistence.api.entity.Delegation;
54  import org.apache.syncope.core.persistence.api.entity.DynRealm;
55  import org.apache.syncope.core.persistence.api.entity.ExternalResource;
56  import org.apache.syncope.core.persistence.api.entity.Realm;
57  import org.apache.syncope.core.persistence.api.entity.Role;
58  import org.apache.syncope.core.persistence.api.entity.user.User;
59  import org.apache.syncope.core.provisioning.api.AuditManager;
60  import org.apache.syncope.core.provisioning.api.ConnectorManager;
61  import org.apache.syncope.core.provisioning.api.MappingManager;
62  import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
63  import org.identityconnectors.framework.common.objects.Uid;
64  import org.slf4j.Logger;
65  import org.slf4j.LoggerFactory;
66  import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
67  import org.springframework.security.authentication.DisabledException;
68  import org.springframework.security.core.Authentication;
69  import org.springframework.security.core.userdetails.UsernameNotFoundException;
70  import org.springframework.security.web.authentication.session.SessionAuthenticationException;
71  import org.springframework.transaction.annotation.Transactional;
72  
73  /**
74   * Domain-sensible (via {@code @Transactional}) access to authentication / authorization data.
75   *
76   * @see JWTAuthenticationProvider
77   * @see UsernamePasswordAuthenticationProvider
78   * @see SyncopeAuthenticationDetails
79   */
80  public class AuthDataAccessor {
81  
82      protected static final Logger LOG = LoggerFactory.getLogger(AuthDataAccessor.class);
83  
84      public static final String GROUP_OWNER_ROLE = "GROUP_OWNER";
85  
86      protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
87  
88      protected static final Set<SyncopeGrantedAuthority> ANONYMOUS_AUTHORITIES =
89              Set.of(new SyncopeGrantedAuthority(IdRepoEntitlement.ANONYMOUS));
90  
91      protected static final Set<SyncopeGrantedAuthority> MUST_CHANGE_PASSWORD_AUTHORITIES =
92              Set.of(new SyncopeGrantedAuthority(IdRepoEntitlement.MUST_CHANGE_PASSWORD));
93  
94      protected final SecurityProperties securityProperties;
95  
96      protected final RealmDAO realmDAO;
97  
98      protected final UserDAO userDAO;
99  
100     protected final GroupDAO groupDAO;
101 
102     protected final AnySearchDAO anySearchDAO;
103 
104     protected final AccessTokenDAO accessTokenDAO;
105 
106     protected final ConfParamOps confParamOps;
107 
108     protected final RoleDAO roleDAO;
109 
110     protected final DelegationDAO delegationDAO;
111 
112     protected final ConnectorManager connectorManager;
113 
114     protected final AuditManager auditManager;
115 
116     protected final MappingManager mappingManager;
117 
118     private final List<JWTSSOProvider> jwtSSOProviders;
119 
120     public AuthDataAccessor(
121             final SecurityProperties securityProperties,
122             final RealmDAO realmDAO,
123             final UserDAO userDAO,
124             final GroupDAO groupDAO,
125             final AnySearchDAO anySearchDAO,
126             final AccessTokenDAO accessTokenDAO,
127             final ConfParamOps confParamOps,
128             final RoleDAO roleDAO,
129             final DelegationDAO delegationDAO,
130             final ConnectorManager connectorManager,
131             final AuditManager auditManager,
132             final MappingManager mappingManager,
133             final List<JWTSSOProvider> jwtSSOProviders) {
134 
135         this.securityProperties = securityProperties;
136         this.realmDAO = realmDAO;
137         this.userDAO = userDAO;
138         this.groupDAO = groupDAO;
139         this.anySearchDAO = anySearchDAO;
140         this.accessTokenDAO = accessTokenDAO;
141         this.confParamOps = confParamOps;
142         this.roleDAO = roleDAO;
143         this.delegationDAO = delegationDAO;
144         this.connectorManager = connectorManager;
145         this.auditManager = auditManager;
146         this.mappingManager = mappingManager;
147         this.jwtSSOProviders = jwtSSOProviders;
148     }
149 
150     public JWTSSOProvider getJWTSSOProvider(final String issuer) {
151         if (issuer == null) {
152             throw new AuthenticationCredentialsNotFoundException("A null issuer is not permitted");
153         }
154 
155         return jwtSSOProviders.stream().filter(provider -> issuer.equals(provider.getIssuer())).findFirst().
156                 orElseThrow(() -> new AuthenticationCredentialsNotFoundException(
157                 "Could not find any registered JWTSSOProvider for issuer " + issuer));
158     }
159 
160     protected String getDelegationKey(final SyncopeAuthenticationDetails details, final String delegatedKey) {
161         if (details.getDelegatedBy() == null) {
162             return null;
163         }
164 
165         String delegatingKey = SyncopeConstants.UUID_PATTERN.matcher(details.getDelegatedBy()).matches()
166                 ? details.getDelegatedBy()
167                 : userDAO.findKey(details.getDelegatedBy());
168         if (delegatingKey == null) {
169             throw new SessionAuthenticationException(
170                     "Delegating user " + details.getDelegatedBy() + " cannot be found");
171         }
172 
173         LOG.debug("Delegation request: delegating:{}, delegated:{}", delegatingKey, delegatedKey);
174 
175         return delegationDAO.findValidFor(delegatingKey, delegatedKey).
176                 orElseThrow(() -> new SessionAuthenticationException(
177                 "Delegation by " + delegatingKey + " was requested but none found"));
178     }
179 
180     /**
181      * Attempts to authenticate the given credentials against internal storage and pass-through resources (if
182      * configured): the first succeeding causes global success.
183      *
184      * @param domain domain
185      * @param authentication given credentials
186      * @return {@code null} if no matching user was found, authentication result otherwise
187      */
188     @Transactional(noRollbackFor = DisabledException.class)
189     public Triple<User, Boolean, String> authenticate(final String domain, final Authentication authentication) {
190         User user = null;
191 
192         String[] authAttrValues = confParamOps.get(
193                 domain, "authentication.attributes", new String[] { "username" }, String[].class);
194         for (int i = 0; user == null && i < authAttrValues.length; i++) {
195             if ("username".equals(authAttrValues[i])) {
196                 user = userDAO.findByUsername(authentication.getName());
197             } else {
198                 AttrCond attrCond = new AttrCond(AttrCond.Type.EQ);
199                 attrCond.setSchema(authAttrValues[i]);
200                 attrCond.setExpression(authentication.getName());
201                 try {
202                     List<User> users = anySearchDAO.search(SearchCond.getLeaf(attrCond), AnyTypeKind.USER);
203                     if (users.size() == 1) {
204                         user = users.get(0);
205                     } else {
206                         LOG.warn("Search condition {} does not uniquely match a user", attrCond);
207                     }
208                 } catch (IllegalArgumentException e) {
209                     LOG.error("While searching user for authentication via {}", attrCond, e);
210                 }
211             }
212         }
213 
214         Boolean authenticated = null;
215         String delegationKey = null;
216         if (user != null) {
217             authenticated = false;
218 
219             if (user.isSuspended() != null && user.isSuspended()) {
220                 throw new DisabledException("User " + user.getUsername() + " is suspended");
221             }
222 
223             String[] authStatuses = confParamOps.get(
224                     domain, "authentication.statuses", new String[] {}, String[].class);
225             if (!ArrayUtils.contains(authStatuses, user.getStatus())) {
226                 throw new DisabledException("User " + user.getUsername() + " not allowed to authenticate");
227             }
228 
229             boolean userModified = false;
230             authenticated = authenticate(user, authentication.getCredentials().toString());
231             if (authenticated) {
232                 delegationKey = getDelegationKey(
233                         SyncopeAuthenticationDetails.class.cast(authentication.getDetails()), user.getKey());
234 
235                 if (confParamOps.get(domain, "log.lastlogindate", true, Boolean.class)) {
236                     user.setLastLoginDate(OffsetDateTime.now());
237                     userModified = true;
238                 }
239 
240                 if (user.getFailedLogins() != 0) {
241                     user.setFailedLogins(0);
242                     userModified = true;
243                 }
244             } else {
245                 user.setFailedLogins(user.getFailedLogins() + 1);
246                 userModified = true;
247             }
248 
249             if (userModified) {
250                 userDAO.save(user);
251             }
252         }
253 
254         return Triple.of(user, authenticated, delegationKey);
255     }
256 
257     protected boolean authenticate(final User user, final String password) {
258         boolean authenticated = ENCRYPTOR.verify(password, user.getCipherAlgorithm(), user.getPassword());
259         LOG.debug("{} authenticated on internal storage: {}", user.getUsername(), authenticated);
260 
261         for (Iterator<? extends ExternalResource> itor = getPassthroughResources(user).iterator();
262                 itor.hasNext() && !authenticated;) {
263 
264             ExternalResource resource = itor.next();
265             String connObjectKey = null;
266             try {
267                 Provision provision = resource.getProvisionByAnyType(AnyTypeKind.USER.name()).
268                         orElseThrow(() -> new AccountNotFoundException(
269                         "Unable to locate provision for user type " + AnyTypeKind.USER.name()));
270                 connObjectKey = mappingManager.getConnObjectKeyValue(user, resource, provision).
271                         orElseThrow(() -> new AccountNotFoundException(
272                         "Unable to locate conn object key value for " + AnyTypeKind.USER.name()));
273                 Uid uid = connectorManager.getConnector(resource).authenticate(connObjectKey, password, null);
274                 if (uid != null) {
275                     authenticated = true;
276                 }
277             } catch (Exception e) {
278                 LOG.debug("Could not authenticate {} on {}", user.getUsername(), resource.getKey(), e);
279             }
280             LOG.debug("{} authenticated on {} as {}: {}",
281                     user.getUsername(), resource.getKey(), connObjectKey, authenticated);
282         }
283 
284         return authenticated;
285     }
286 
287     protected Set<? extends ExternalResource> getPassthroughResources(final User user) {
288         Set<? extends ExternalResource> result = null;
289 
290         // 1. look for assigned resources, pick the ones whose account policy has authentication resources
291         for (ExternalResource resource : userDAO.findAllResources(user)) {
292             if (resource.getAccountPolicy() != null && !resource.getAccountPolicy().getResources().isEmpty()) {
293                 if (result == null) {
294                     result = resource.getAccountPolicy().getResources();
295                 } else {
296                     result.retainAll(resource.getAccountPolicy().getResources());
297                 }
298             }
299         }
300 
301         // 2. look for realms, pick the ones whose account policy has authentication resources
302         for (Realm realm : realmDAO.findAncestors(user.getRealm())) {
303             if (realm.getAccountPolicy() != null && !realm.getAccountPolicy().getResources().isEmpty()) {
304                 if (result == null) {
305                     result = realm.getAccountPolicy().getResources();
306                 } else {
307                     result.retainAll(realm.getAccountPolicy().getResources());
308                 }
309             }
310         }
311 
312         return result == null ? Set.of() : result;
313     }
314 
315     protected Set<SyncopeGrantedAuthority> getAdminAuthorities() {
316         return EntitlementsHolder.getInstance().getValues().stream().
317                 map(entitlement -> new SyncopeGrantedAuthority(entitlement, SyncopeConstants.ROOT_REALM)).
318                 collect(Collectors.toSet());
319     }
320 
321     protected Set<SyncopeGrantedAuthority> buildAuthorities(final Map<String, Set<String>> entForRealms) {
322         Set<SyncopeGrantedAuthority> authorities = new HashSet<>();
323 
324         entForRealms.forEach((entitlement, realms) -> {
325             Pair<Set<String>, Set<String>> normalized = RealmUtils.normalize(realms);
326 
327             SyncopeGrantedAuthority authority = new SyncopeGrantedAuthority(entitlement);
328             authority.addRealms(normalized.getLeft());
329             authority.addRealms(normalized.getRight());
330             authorities.add(authority);
331         });
332 
333         return authorities;
334     }
335 
336     protected Set<SyncopeGrantedAuthority> getUserAuthorities(final User user) {
337         if (user.isMustChangePassword()) {
338             return MUST_CHANGE_PASSWORD_AUTHORITIES;
339         }
340 
341         Map<String, Set<String>> entForRealms = new HashMap<>();
342 
343         // Give entitlements as assigned by roles (with static or dynamic realms, where applicable) - assigned
344         // either statically and dynamically
345         userDAO.findAllRoles(user).stream().
346                 filter(role -> !GROUP_OWNER_ROLE.equals(role.getKey())).
347                 forEach(role -> role.getEntitlements().forEach(entitlement -> {
348             Set<String> realms = Optional.ofNullable(entForRealms.get(entitlement)).orElseGet(() -> {
349                 Set<String> r = new HashSet<>();
350                 entForRealms.put(entitlement, r);
351                 return r;
352             });
353 
354             realms.addAll(role.getRealms().stream().map(Realm::getFullPath).collect(Collectors.toSet()));
355             if (!entitlement.endsWith("_CREATE") && !entitlement.endsWith("_DELETE")) {
356                 realms.addAll(role.getDynRealms().stream().map(DynRealm::getKey).collect(Collectors.toList()));
357             }
358         }));
359 
360         // Give group entitlements for owned groups
361         groupDAO.findOwnedByUser(user.getKey()).forEach(group -> {
362             Role groupOwnerRole = roleDAO.find(GROUP_OWNER_ROLE);
363             if (groupOwnerRole == null) {
364                 LOG.warn("Role {} was not found", GROUP_OWNER_ROLE);
365             } else {
366                 groupOwnerRole.getEntitlements().forEach(entitlement -> {
367                     Set<String> realms = Optional.ofNullable(entForRealms.get(entitlement)).orElseGet(() -> {
368                         HashSet<String> r = new HashSet<>();
369                         entForRealms.put(entitlement, r);
370                         return r;
371                     });
372 
373                     realms.add(RealmUtils.getGroupOwnerRealm(group.getRealm().getFullPath(), group.getKey()));
374                 });
375             }
376         });
377 
378         return buildAuthorities(entForRealms);
379     }
380 
381     protected Set<SyncopeGrantedAuthority> getDelegatedAuthorities(final Delegation delegation) {
382         Map<String, Set<String>> entForRealms = new HashMap<>();
383 
384         delegation.getRoles().stream().filter(role -> !GROUP_OWNER_ROLE.equals(role.getKey())).
385                 forEach(role -> role.getEntitlements().forEach(entitlement -> {
386             Set<String> realms = Optional.ofNullable(entForRealms.get(entitlement)).orElseGet(() -> {
387                 HashSet<String> r = new HashSet<>();
388                 entForRealms.put(entitlement, r);
389                 return r;
390             });
391 
392             realms.addAll(role.getRealms().stream().map(Realm::getFullPath).collect(Collectors.toSet()));
393             if (!entitlement.endsWith("_CREATE") && !entitlement.endsWith("_DELETE")) {
394                 realms.addAll(role.getDynRealms().stream().map(DynRealm::getKey).collect(Collectors.toList()));
395             }
396         }));
397 
398         return buildAuthorities(entForRealms);
399     }
400 
401     @Transactional
402     public Set<SyncopeGrantedAuthority> getAuthorities(final String username, final String delegationKey) {
403         Set<SyncopeGrantedAuthority> authorities;
404 
405         if (securityProperties.getAnonymousUser().equals(username)) {
406             authorities = ANONYMOUS_AUTHORITIES;
407         } else if (securityProperties.getAdminUser().equals(username)) {
408             authorities = getAdminAuthorities();
409         } else if (delegationKey != null) {
410             Delegation delegation = Optional.ofNullable(delegationDAO.find(delegationKey)).
411                     orElseThrow(() -> new UsernameNotFoundException(
412                     "Could not find delegation " + delegationKey));
413 
414             authorities = delegation.getRoles().isEmpty()
415                     ? getUserAuthorities(delegation.getDelegating())
416                     : getDelegatedAuthorities(delegation);
417         } else {
418             User user = Optional.ofNullable(userDAO.findByUsername(username)).
419                     orElseThrow(() -> new UsernameNotFoundException(
420                     "Could not find any user with username " + username));
421 
422             authorities = getUserAuthorities(user);
423         }
424 
425         return authorities;
426     }
427 
428     @Transactional
429     public Pair<String, Set<SyncopeGrantedAuthority>> authenticate(final JWTAuthentication authentication) {
430         String username;
431         Set<SyncopeGrantedAuthority> authorities;
432 
433         if (securityProperties.getAdminUser().equals(authentication.getClaims().getSubject())) {
434             AccessToken accessToken = accessTokenDAO.find(authentication.getClaims().getJWTID());
435             if (accessToken == null) {
436                 throw new AuthenticationCredentialsNotFoundException(
437                         "Could not find an Access Token for JWT " + authentication.getClaims().getJWTID());
438             }
439 
440             username = securityProperties.getAdminUser();
441             authorities = getAdminAuthorities();
442         } else {
443             JWTSSOProvider jwtSSOProvider = getJWTSSOProvider(authentication.getClaims().getIssuer());
444             Pair<User, Set<SyncopeGrantedAuthority>> resolved = jwtSSOProvider.resolve(authentication.getClaims());
445             if (resolved == null || resolved.getLeft() == null) {
446                 throw new AuthenticationCredentialsNotFoundException(
447                         "Could not find User " + authentication.getClaims().getSubject()
448                         + " for JWT " + authentication.getClaims().getJWTID());
449             }
450 
451             User user = resolved.getLeft();
452             String delegationKey = getDelegationKey(authentication.getDetails(), user.getKey());
453             username = user.getUsername();
454             authorities = resolved.getRight() == null
455                     ? Set.of()
456                     : delegationKey == null
457                             ? resolved.getRight()
458                             : getAuthorities(username, delegationKey);
459             LOG.debug("JWT {} issued by {} resolved to User {} with authorities {}",
460                     authentication.getClaims().getJWTID(),
461                     authentication.getClaims().getIssuer(),
462                     username + Optional.ofNullable(delegationKey).
463                             map(d -> " [under delegation " + delegationKey + "]").orElse(StringUtils.EMPTY),
464                     authorities);
465 
466             if (BooleanUtils.isTrue(user.isSuspended())) {
467                 throw new DisabledException("User " + username + " is suspended");
468             }
469 
470             List<String> authStatuses = List.of(confParamOps.get(authentication.getDetails().getDomain(),
471                     "authentication.statuses", new String[] {}, String[].class));
472             if (!authStatuses.contains(user.getStatus())) {
473                 throw new DisabledException("User " + username + " not allowed to authenticate");
474             }
475 
476             if (BooleanUtils.isTrue(user.isMustChangePassword())) {
477                 LOG.debug("User {} must change password, resetting authorities", username);
478                 authorities = MUST_CHANGE_PASSWORD_AUTHORITIES;
479             }
480         }
481 
482         return Pair.of(username, authorities);
483     }
484 
485     @Transactional
486     public void removeExpired(final String tokenKey) {
487         accessTokenDAO.delete(tokenKey);
488     }
489 
490     @Transactional(readOnly = true)
491     public void audit(
492             final String username,
493             final String delegationKey,
494             final AuditElements.Result result,
495             final Object output,
496             final Object... input) {
497 
498         auditManager.audit(
499                 username + Optional.ofNullable(delegationKey).
500                         map(d -> " [under delegation " + delegationKey + "]").orElse(StringUtils.EMPTY),
501                 AuditElements.EventCategoryType.LOGIC, AuditElements.AUTHENTICATION_CATEGORY, null,
502                 AuditElements.LOGIN_EVENT, result, null, output, input);
503     }
504 }