| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.syncope.core.spring.security; |
| |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import javax.security.auth.login.AccountNotFoundException; |
| import org.apache.commons.lang3.ArrayUtils; |
| import org.apache.commons.lang3.BooleanUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.commons.lang3.tuple.Pair; |
| import org.apache.commons.lang3.tuple.Triple; |
| import org.apache.syncope.common.keymaster.client.api.ConfParamOps; |
| import org.apache.syncope.common.lib.SyncopeConstants; |
| import org.apache.syncope.common.lib.types.AnyTypeKind; |
| import org.apache.syncope.common.lib.types.AuditElements; |
| import org.apache.syncope.common.lib.types.EntitlementsHolder; |
| import org.apache.syncope.common.lib.types.IdRepoEntitlement; |
| import org.apache.syncope.core.persistence.api.ImplementationLookup; |
| import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO; |
| import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; |
| import org.apache.syncope.core.persistence.api.entity.AnyType; |
| import org.apache.syncope.core.persistence.api.entity.resource.Provision; |
| import org.apache.syncope.core.provisioning.api.utils.RealmUtils; |
| import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO; |
| import org.apache.syncope.core.persistence.api.dao.DelegationDAO; |
| import org.apache.syncope.core.persistence.api.dao.GroupDAO; |
| import org.apache.syncope.core.persistence.api.dao.RealmDAO; |
| import org.apache.syncope.core.persistence.api.dao.RoleDAO; |
| import org.apache.syncope.core.persistence.api.dao.UserDAO; |
| import org.apache.syncope.core.persistence.api.dao.search.AttrCond; |
| import org.apache.syncope.core.persistence.api.dao.search.SearchCond; |
| import org.apache.syncope.core.persistence.api.entity.AccessToken; |
| import org.apache.syncope.core.persistence.api.entity.Delegation; |
| import org.apache.syncope.core.persistence.api.entity.DynRealm; |
| import org.apache.syncope.core.persistence.api.entity.Realm; |
| import org.apache.syncope.core.persistence.api.entity.Role; |
| import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource; |
| import org.apache.syncope.core.persistence.api.entity.user.User; |
| import org.apache.syncope.core.provisioning.api.AuditManager; |
| import org.apache.syncope.core.provisioning.api.ConnectorFactory; |
| import org.apache.syncope.core.provisioning.api.MappingManager; |
| import org.apache.syncope.core.spring.ApplicationContextProvider; |
| import org.identityconnectors.framework.common.objects.Uid; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.beans.factory.support.AbstractBeanDefinition; |
| import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; |
| import org.springframework.security.authentication.DisabledException; |
| import org.springframework.security.core.Authentication; |
| import org.springframework.security.core.userdetails.UsernameNotFoundException; |
| import org.springframework.security.web.authentication.session.SessionAuthenticationException; |
| import org.springframework.transaction.annotation.Transactional; |
| |
| /** |
| * Domain-sensible (via {@code @Transactional}) access to authentication / authorization data. |
| * |
| * @see JWTAuthenticationProvider |
| * @see UsernamePasswordAuthenticationProvider |
| * @see SyncopeAuthenticationDetails |
| */ |
| public class AuthDataAccessor { |
| |
| protected static final Logger LOG = LoggerFactory.getLogger(AuthDataAccessor.class); |
| |
| protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); |
| |
| protected static final Set<SyncopeGrantedAuthority> ANONYMOUS_AUTHORITIES = |
| Set.of(new SyncopeGrantedAuthority(IdRepoEntitlement.ANONYMOUS)); |
| |
| protected static final Set<SyncopeGrantedAuthority> MUST_CHANGE_PASSWORD_AUTHORITIES = |
| Set.of(new SyncopeGrantedAuthority(IdRepoEntitlement.MUST_CHANGE_PASSWORD)); |
| |
| @Autowired |
| protected SecurityProperties securityProperties; |
| |
| @Autowired |
| protected RealmDAO realmDAO; |
| |
| @Autowired |
| protected UserDAO userDAO; |
| |
| @Autowired |
| protected GroupDAO groupDAO; |
| |
| @Autowired |
| protected AnyTypeDAO anyTypeDAO; |
| |
| @Autowired |
| protected AnySearchDAO searchDAO; |
| |
| @Autowired |
| protected AccessTokenDAO accessTokenDAO; |
| |
| @Autowired |
| protected ConfParamOps confParamOps; |
| |
| @Autowired |
| protected RoleDAO roleDAO; |
| |
| @Autowired |
| protected DelegationDAO delegationDAO; |
| |
| @Autowired |
| protected ConnectorFactory connFactory; |
| |
| @Autowired |
| protected AuditManager auditManager; |
| |
| @Autowired |
| protected MappingManager mappingManager; |
| |
| @Autowired |
| protected ImplementationLookup implementationLookup; |
| |
| private Map<String, JWTSSOProvider> jwtSSOProviders; |
| |
| public JWTSSOProvider getJWTSSOProvider(final String issuer) { |
| synchronized (this) { |
| if (jwtSSOProviders == null) { |
| jwtSSOProviders = new HashMap<>(); |
| |
| implementationLookup.getJWTSSOProviderClasses().stream(). |
| map(clazz -> (JWTSSOProvider) ApplicationContextProvider.getBeanFactory(). |
| createBean(clazz, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true)). |
| forEach(jwtSSOProvider -> jwtSSOProviders.put(jwtSSOProvider.getIssuer(), jwtSSOProvider)); |
| } |
| } |
| |
| if (issuer == null) { |
| throw new AuthenticationCredentialsNotFoundException("A null issuer is not permitted"); |
| } |
| JWTSSOProvider provider = jwtSSOProviders.get(issuer); |
| if (provider == null) { |
| throw new AuthenticationCredentialsNotFoundException( |
| "Could not find any registered JWTSSOProvider for issuer " + issuer); |
| } |
| |
| return provider; |
| } |
| |
| protected String getDelegationKey(final SyncopeAuthenticationDetails details, final String delegatedKey) { |
| return Optional.ofNullable(details.getDelegatedBy()). |
| map(delegatingKey -> SyncopeConstants.UUID_PATTERN.matcher(delegatingKey).matches() |
| ? delegatingKey |
| : userDAO.findKey(delegatingKey)).map(delegatingKey -> { |
| |
| LOG.debug("Delegation request: delegating:{}, delegated:{}", delegatingKey, delegatedKey); |
| |
| return delegationDAO.findValidFor(delegatingKey, delegatedKey). |
| orElseThrow(() -> new SessionAuthenticationException( |
| "Delegation by " + delegatingKey + " was requested but none found")); |
| }).orElse(null); |
| } |
| |
| /** |
| * Attempts to authenticate the given credentials against internal storage and pass-through resources (if |
| * configured): the first succeeding causes global success. |
| * |
| * @param domain domain |
| * @param authentication given credentials |
| * @return {@code null} if no matching user was found, authentication result otherwise |
| */ |
| @Transactional(noRollbackFor = DisabledException.class) |
| public Triple<User, Boolean, String> authenticate(final String domain, final Authentication authentication) { |
| User user = null; |
| |
| List<String> authAttrValues = List.of(confParamOps.get(domain, |
| "authentication.attributes", new String[] { "username" }, String[].class)); |
| for (int i = 0; user == null && i < authAttrValues.size(); i++) { |
| if ("username".equals(authAttrValues.get(i))) { |
| user = userDAO.findByUsername(authentication.getName()); |
| } else { |
| AttrCond attrCond = new AttrCond(AttrCond.Type.EQ); |
| attrCond.setSchema(authAttrValues.get(i)); |
| attrCond.setExpression(authentication.getName()); |
| List<User> users = searchDAO.search(SearchCond.getLeaf(attrCond), AnyTypeKind.USER); |
| if (users.size() == 1) { |
| user = users.get(0); |
| } else { |
| LOG.warn("Value {} provided for {} does not uniquely identify a user", |
| authentication.getName(), authAttrValues.get(i)); |
| } |
| } |
| } |
| |
| Boolean authenticated = null; |
| String delegationKey = null; |
| if (user != null) { |
| authenticated = false; |
| |
| if (user.isSuspended() != null && user.isSuspended()) { |
| throw new DisabledException("User " + user.getUsername() + " is suspended"); |
| } |
| |
| String[] authStatuses = confParamOps.get( |
| domain, "authentication.statuses", new String[] {}, String[].class); |
| if (!ArrayUtils.contains(authStatuses, user.getStatus())) { |
| throw new DisabledException("User " + user.getUsername() + " not allowed to authenticate"); |
| } |
| |
| boolean userModified = false; |
| authenticated = authenticate(user, authentication.getCredentials().toString()); |
| if (authenticated) { |
| delegationKey = getDelegationKey( |
| SyncopeAuthenticationDetails.class.cast(authentication.getDetails()), user.getKey()); |
| |
| if (confParamOps.get(domain, "log.lastlogindate", true, Boolean.class)) { |
| user.setLastLoginDate(new Date()); |
| userModified = true; |
| } |
| |
| if (user.getFailedLogins() != 0) { |
| user.setFailedLogins(0); |
| userModified = true; |
| } |
| } else { |
| user.setFailedLogins(user.getFailedLogins() + 1); |
| userModified = true; |
| } |
| |
| if (userModified) { |
| userDAO.save(user); |
| } |
| } |
| |
| return Triple.of(user, authenticated, delegationKey); |
| } |
| |
| protected boolean authenticate(final User user, final String password) { |
| boolean authenticated = ENCRYPTOR.verify(password, user.getCipherAlgorithm(), user.getPassword()); |
| LOG.debug("{} authenticated on internal storage: {}", user.getUsername(), authenticated); |
| |
| for (Iterator<? extends ExternalResource> itor = getPassthroughResources(user).iterator(); |
| itor.hasNext() && !authenticated;) { |
| |
| ExternalResource resource = itor.next(); |
| String connObjectKey = null; |
| try { |
| AnyType userType = anyTypeDAO.findUser(); |
| Provision provision = resource.getProvision(userType). |
| orElseThrow(() -> new AccountNotFoundException( |
| "Unable to locate provision for user type " + userType.getKey())); |
| connObjectKey = mappingManager.getConnObjectKeyValue(user, provision). |
| orElseThrow(() -> new AccountNotFoundException( |
| "Unable to locate conn object key value for " + userType.getKey())); |
| Uid uid = connFactory.getConnector(resource).authenticate(connObjectKey, password, null); |
| if (uid != null) { |
| authenticated = true; |
| } |
| } catch (Exception e) { |
| LOG.debug("Could not authenticate {} on {}", user.getUsername(), resource.getKey(), e); |
| } |
| LOG.debug("{} authenticated on {} as {}: {}", |
| user.getUsername(), resource.getKey(), connObjectKey, authenticated); |
| } |
| |
| return authenticated; |
| } |
| |
| protected Set<? extends ExternalResource> getPassthroughResources(final User user) { |
| Set<? extends ExternalResource> result = null; |
| |
| // 1. look for assigned resources, pick the ones whose account policy has authentication resources |
| for (ExternalResource resource : userDAO.findAllResources(user)) { |
| if (resource.getAccountPolicy() != null && !resource.getAccountPolicy().getResources().isEmpty()) { |
| if (result == null) { |
| result = resource.getAccountPolicy().getResources(); |
| } else { |
| result.retainAll(resource.getAccountPolicy().getResources()); |
| } |
| } |
| } |
| |
| // 2. look for realms, pick the ones whose account policy has authentication resources |
| for (Realm realm : realmDAO.findAncestors(user.getRealm())) { |
| if (realm.getAccountPolicy() != null && !realm.getAccountPolicy().getResources().isEmpty()) { |
| if (result == null) { |
| result = realm.getAccountPolicy().getResources(); |
| } else { |
| result.retainAll(realm.getAccountPolicy().getResources()); |
| } |
| } |
| } |
| |
| return result == null ? Set.of() : result; |
| } |
| |
| protected Set<SyncopeGrantedAuthority> getAdminAuthorities() { |
| return EntitlementsHolder.getInstance().getValues().stream(). |
| map(entitlement -> new SyncopeGrantedAuthority(entitlement, SyncopeConstants.ROOT_REALM)). |
| collect(Collectors.toSet()); |
| } |
| |
| protected Set<SyncopeGrantedAuthority> buildAuthorities(final Map<String, Set<String>> entForRealms) { |
| Set<SyncopeGrantedAuthority> authorities = new HashSet<>(); |
| |
| entForRealms.forEach((entitlement, realms) -> { |
| Pair<Set<String>, Set<String>> normalized = RealmUtils.normalize(realms); |
| |
| SyncopeGrantedAuthority authority = new SyncopeGrantedAuthority(entitlement); |
| authority.addRealms(normalized.getLeft()); |
| authority.addRealms(normalized.getRight()); |
| authorities.add(authority); |
| }); |
| |
| return authorities; |
| } |
| |
| protected Set<SyncopeGrantedAuthority> getUserAuthorities(final User user) { |
| if (user.isMustChangePassword()) { |
| return MUST_CHANGE_PASSWORD_AUTHORITIES; |
| } |
| |
| Map<String, Set<String>> entForRealms = new HashMap<>(); |
| |
| // Give entitlements as assigned by roles (with static or dynamic realms, where applicable) - assigned |
| // either statically and dynamically |
| userDAO.findAllRoles(user).stream(). |
| filter(role -> !SyncopeConstants.GROUP_OWNER_ROLE.equals(role.getKey())). |
| forEach(role -> role.getEntitlements().forEach(entitlement -> { |
| Set<String> realms = Optional.ofNullable(entForRealms.get(entitlement)).orElseGet(() -> { |
| Set<String> r = new HashSet<>(); |
| entForRealms.put(entitlement, r); |
| return r; |
| }); |
| |
| realms.addAll(role.getRealms().stream().map(Realm::getFullPath).collect(Collectors.toSet())); |
| if (!entitlement.endsWith("_CREATE") && !entitlement.endsWith("_DELETE")) { |
| realms.addAll(role.getDynRealms().stream().map(DynRealm::getKey).collect(Collectors.toList())); |
| } |
| })); |
| |
| // Give group entitlements for owned groups |
| groupDAO.findOwnedByUser(user.getKey()).forEach(group -> { |
| Role groupOwnerRole = roleDAO.find(SyncopeConstants.GROUP_OWNER_ROLE); |
| if (groupOwnerRole == null) { |
| LOG.warn("Role {} was not found", SyncopeConstants.GROUP_OWNER_ROLE); |
| } else { |
| groupOwnerRole.getEntitlements().forEach(entitlement -> { |
| Set<String> realms = Optional.ofNullable(entForRealms.get(entitlement)).orElseGet(() -> { |
| HashSet<String> r = new HashSet<>(); |
| entForRealms.put(entitlement, r); |
| return r; |
| }); |
| |
| realms.add(RealmUtils.getGroupOwnerRealm(group.getRealm().getFullPath(), group.getKey())); |
| }); |
| } |
| }); |
| |
| return buildAuthorities(entForRealms); |
| } |
| |
| protected Set<SyncopeGrantedAuthority> getDelegatedAuthorities(final Delegation delegation) { |
| Map<String, Set<String>> entForRealms = new HashMap<>(); |
| |
| delegation.getRoles().stream().filter(role -> !SyncopeConstants.GROUP_OWNER_ROLE.equals(role.getKey())). |
| forEach(role -> role.getEntitlements().forEach(entitlement -> { |
| Set<String> realms = Optional.ofNullable(entForRealms.get(entitlement)).orElseGet(() -> { |
| HashSet<String> r = new HashSet<>(); |
| entForRealms.put(entitlement, r); |
| return r; |
| }); |
| |
| realms.addAll(role.getRealms().stream().map(Realm::getFullPath).collect(Collectors.toSet())); |
| if (!entitlement.endsWith("_CREATE") && !entitlement.endsWith("_DELETE")) { |
| realms.addAll(role.getDynRealms().stream().map(DynRealm::getKey).collect(Collectors.toList())); |
| } |
| })); |
| |
| return buildAuthorities(entForRealms); |
| } |
| |
| @Transactional |
| public Set<SyncopeGrantedAuthority> getAuthorities(final String username, final String delegationKey) { |
| Set<SyncopeGrantedAuthority> authorities; |
| |
| if (securityProperties.getAnonymousUser().equals(username)) { |
| authorities = ANONYMOUS_AUTHORITIES; |
| } else if (securityProperties.getAdminUser().equals(username)) { |
| authorities = getAdminAuthorities(); |
| } else if (delegationKey != null) { |
| Delegation delegation = Optional.ofNullable(delegationDAO.find(delegationKey)). |
| orElseThrow(() -> new UsernameNotFoundException( |
| "Could not find delegation " + delegationKey)); |
| |
| authorities = delegation.getRoles().isEmpty() |
| ? getUserAuthorities(delegation.getDelegating()) |
| : getDelegatedAuthorities(delegation); |
| } else { |
| User user = Optional.ofNullable(userDAO.findByUsername(username)). |
| orElseThrow(() -> new UsernameNotFoundException( |
| "Could not find any user with username " + username)); |
| |
| authorities = getUserAuthorities(user); |
| } |
| |
| return authorities; |
| } |
| |
| @Transactional |
| public Pair<String, Set<SyncopeGrantedAuthority>> authenticate(final JWTAuthentication authentication) { |
| String username; |
| Set<SyncopeGrantedAuthority> authorities; |
| |
| if (securityProperties.getAdminUser().equals(authentication.getClaims().getSubject())) { |
| AccessToken accessToken = accessTokenDAO.find(authentication.getClaims().getJWTID()); |
| if (accessToken == null) { |
| throw new AuthenticationCredentialsNotFoundException( |
| "Could not find an Access Token for JWT " + authentication.getClaims().getJWTID()); |
| } |
| |
| username = securityProperties.getAdminUser(); |
| authorities = getAdminAuthorities(); |
| } else { |
| JWTSSOProvider jwtSSOProvider = getJWTSSOProvider(authentication.getClaims().getIssuer()); |
| Pair<User, Set<SyncopeGrantedAuthority>> resolved = jwtSSOProvider.resolve(authentication.getClaims()); |
| if (resolved == null || resolved.getLeft() == null) { |
| throw new AuthenticationCredentialsNotFoundException( |
| "Could not find User " + authentication.getClaims().getSubject() |
| + " for JWT " + authentication.getClaims().getJWTID()); |
| } |
| |
| User user = resolved.getLeft(); |
| String delegationKey = getDelegationKey(authentication.getDetails(), user.getKey()); |
| username = user.getUsername(); |
| authorities = resolved.getRight() == null |
| ? Set.of() |
| : delegationKey == null |
| ? resolved.getRight() |
| : getAuthorities(username, delegationKey); |
| LOG.debug("JWT {} issued by {} resolved to User {} with authorities {}", |
| authentication.getClaims().getJWTID(), |
| authentication.getClaims().getIssuer(), |
| username + Optional.ofNullable(delegationKey). |
| map(d -> " [under delegation " + delegationKey + "]").orElse(StringUtils.EMPTY), |
| authorities); |
| |
| if (BooleanUtils.isTrue(user.isSuspended())) { |
| throw new DisabledException("User " + username + " is suspended"); |
| } |
| |
| List<String> authStatuses = List.of(confParamOps.get(authentication.getDetails().getDomain(), |
| "authentication.statuses", new String[] {}, String[].class)); |
| if (!authStatuses.contains(user.getStatus())) { |
| throw new DisabledException("User " + username + " not allowed to authenticate"); |
| } |
| |
| if (BooleanUtils.isTrue(user.isMustChangePassword())) { |
| LOG.debug("User {} must change password, resetting authorities", username); |
| authorities = MUST_CHANGE_PASSWORD_AUTHORITIES; |
| } |
| } |
| |
| return Pair.of(username, authorities); |
| } |
| |
| @Transactional |
| public void removeExpired(final String tokenKey) { |
| accessTokenDAO.delete(tokenKey); |
| } |
| |
| @Transactional(readOnly = true) |
| public void audit( |
| final String username, |
| final String delegationKey, |
| final AuditElements.Result result, |
| final Object output, |
| final Object... input) { |
| |
| auditManager.audit( |
| username + Optional.ofNullable(delegationKey). |
| map(d -> " [under delegation " + delegationKey + "]").orElse(StringUtils.EMPTY), |
| AuditElements.EventCategoryType.LOGIC, AuditElements.AUTHENTICATION_CATEGORY, null, |
| AuditElements.LOGIN_EVENT, result, null, output, input); |
| } |
| } |