1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
75
76
77
78
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
182
183
184
185
186
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
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
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
344
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
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 }