| // 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.cloudstack.ldap; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| |
| import javax.inject.Inject; |
| |
| import org.apache.cloudstack.acl.RoleType; |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.log4j.Logger; |
| |
| import com.cloud.server.auth.UserAuthenticator; |
| import com.cloud.user.Account; |
| import com.cloud.user.AccountManager; |
| import com.cloud.user.User; |
| import com.cloud.user.UserAccount; |
| import com.cloud.user.dao.UserAccountDao; |
| import com.cloud.utils.Pair; |
| import com.cloud.utils.component.AdapterBase; |
| |
| public class LdapAuthenticator extends AdapterBase implements UserAuthenticator { |
| private static final Logger s_logger = Logger.getLogger(LdapAuthenticator.class.getName()); |
| |
| @Inject |
| private LdapManager _ldapManager; |
| @Inject |
| private UserAccountDao _userAccountDao; |
| @Inject |
| private AccountManager _accountManager; |
| |
| public LdapAuthenticator() { |
| super(); |
| } |
| |
| public LdapAuthenticator(final LdapManager ldapManager, final UserAccountDao userAccountDao) { |
| super(); |
| _ldapManager = ldapManager; |
| _userAccountDao = userAccountDao; |
| } |
| |
| @Override |
| public Pair<Boolean, ActionOnFailedAuthentication> authenticate(final String username, final String password, final Long domainId, final Map<String, Object[]> requestParameters) { |
| Pair<Boolean, ActionOnFailedAuthentication> rc = new Pair<Boolean, ActionOnFailedAuthentication>(false, null); |
| |
| // TODO not allowing an empty password is a policy we shouldn't decide on. A private cloud may well want to allow this. |
| if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { |
| s_logger.debug("Username or Password cannot be empty"); |
| return rc; |
| } |
| |
| if (_ldapManager.isLdapEnabled()) { |
| final UserAccount user = _userAccountDao.getUserAccount(username, domainId); |
| List<LdapTrustMapVO> ldapTrustMapVOs = _ldapManager.getDomainLinkage(domainId); |
| if(ldapTrustMapVOs != null && ldapTrustMapVOs.size() > 0) { |
| if(ldapTrustMapVOs.size() == 1 && ldapTrustMapVOs.get(0).getAccountId() == 0) { |
| // We have a single mapping of a domain to an ldap group or ou |
| return authenticate(username, password, domainId, user, ldapTrustMapVOs.get(0)); |
| } else { |
| // we are dealing with mapping of accounts in a domain to ldap groups |
| return authenticate(username, password, domainId, user, ldapTrustMapVOs); |
| } |
| } else { |
| //domain is not linked to ldap follow normal authentication |
| return authenticate(username, password, domainId, user); |
| } |
| } |
| |
| return rc; |
| } |
| |
| /** |
| * checks if the user exists in ldap and create in cloudstack if needed. |
| * |
| * @param username login id |
| * @param password pass phrase |
| * @param domainId domain the user is trying to log on to |
| * @param userAccount cloudstack user object |
| * @param ldapTrustMapVOs the trust mappings of accounts in the domain to ldap groups |
| * @return false if the ldap user object does not exist, is not mapped to an account, is mapped to multiple accounts or if authenitication fails |
| */ |
| private Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, UserAccount userAccount, List<LdapTrustMapVO> ldapTrustMapVOs) { |
| Pair<Boolean, ActionOnFailedAuthentication> rc = new Pair<Boolean, ActionOnFailedAuthentication>(false, null); |
| try { |
| LdapUser ldapUser = _ldapManager.getUser(username, domainId); |
| List<String> memberships = ldapUser.getMemberships(); |
| List<String> mappedGroups = getMappedGroups(ldapTrustMapVOs); |
| mappedGroups.retainAll(memberships); |
| // check membership, there must be only one match in this domain |
| if(ldapUser.isDisabled()) { |
| logAndDisable(userAccount, "attempt to log on using disabled ldap user " + userAccount.getUsername(), false); |
| } else if(mappedGroups.size() > 1) { |
| logAndDisable(userAccount, "user '" + username + "' is mapped to more then one account in domain and will be disabled.", false); |
| } else if(mappedGroups.size() < 1) { |
| logAndDisable(userAccount, "user '" + username + "' is not mapped to an account in domain and will be removed.", true); |
| } else { |
| // a valid ldap configured user exists |
| LdapTrustMapVO mapping = _ldapManager.getLinkedLdapGroup(domainId,mappedGroups.get(0)); |
| // we could now assert that ldapTrustMapVOs.contains(mapping); |
| // createUser in Account can only be done by account name not by account id |
| String accountName = _accountManager.getAccount(mapping.getAccountId()).getAccountName(); |
| rc.first(_ldapManager.canAuthenticate(ldapUser.getPrincipal(), password, domainId)); |
| // for security reasons we keep processing on faulty login attempt to not give a way information on userid existence |
| if (userAccount == null) { |
| // new user that is in ldap; authenticate and create |
| User user = _accountManager.createUser(username, "", ldapUser.getFirstname(), ldapUser.getLastname(), ldapUser.getEmail(), null, accountName, |
| domainId, UUID.randomUUID().toString(), User.Source.LDAP); |
| /* expected error conditions: |
| * |
| * caught in APIServlet: CloudRuntimeException("The domain " + domainId + " does not exist; unable to create user"); |
| * caught in APIServlet: CloudRuntimeException("The user cannot be created as domain " + domain.getName() + " is being deleted"); |
| * would have been thrown above: InvalidParameterValueException("Unable to find account " + accountName + " in domain id=" + domainId + " to create user"); |
| * we are system user: PermissionDeniedException("Account id : " + account.getId() + " is a system account, can't add a user to it"); |
| * serious and must be thrown: CloudRuntimeException("The user " + userName + " already exists in domain " + domainId); |
| * fatal system error and must be thrown: CloudRuntimeException("Failed to encode password"); |
| */ |
| userAccount = _accountManager.getUserAccountById(user.getId()); |
| } else { |
| // not a new user, check if mapped group has changed |
| if(userAccount.getAccountId() != mapping.getAccountId()) { |
| _accountManager.moveUser(userAccount.getId(),userAccount.getDomainId(),mapping.getAccountId()); |
| } |
| // else { the user hasn't changed in ldap, the ldap group stayed the same, hurray, pass, fun thou self a lot of fun } |
| } |
| } |
| } catch (NoLdapUserMatchingQueryException e) { |
| s_logger.debug(e.getMessage()); |
| disableUserInCloudStack(userAccount); |
| } |
| |
| return rc; |
| } |
| |
| private void logAndDisable(UserAccount userAccount, String msg, boolean remove) { |
| if (s_logger.isInfoEnabled()) { |
| s_logger.info(msg); |
| } |
| if(remove) { |
| removeUserInCloudStack(userAccount); |
| } else { |
| disableUserInCloudStack(userAccount); |
| } |
| } |
| |
| private List<String> getMappedGroups(List<LdapTrustMapVO> ldapTrustMapVOs) { |
| List<String> groups = new ArrayList<>(); |
| for (LdapTrustMapVO vo : ldapTrustMapVOs) { |
| groups.add(vo.getName()); |
| } |
| return groups; |
| } |
| |
| /** |
| * checks if the user exists in ldap and create in cloudstack if needed |
| * @param username login id |
| * @param password pass phrase |
| * @param domainId domain the user is trying to log on to |
| * @param user cloudstack user object |
| * @param ldapTrustMapVO the trust mapping for the domain to the ldap group |
| * @return false if the ldap user object does not exist or authenitication fails |
| */ |
| private Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, UserAccount user, LdapTrustMapVO ldapTrustMapVO) { |
| Pair<Boolean, ActionOnFailedAuthentication> rc = new Pair<Boolean, ActionOnFailedAuthentication>(false, null); |
| try { |
| LdapUser ldapUser = _ldapManager.getUser(username, ldapTrustMapVO.getType().toString(), ldapTrustMapVO.getName(), domainId); |
| final short accountType = ldapTrustMapVO.getAccountType(); |
| processLdapUser(password, domainId, user, rc, ldapUser, accountType); |
| } catch (NoLdapUserMatchingQueryException e) { |
| s_logger.debug(e.getMessage()); |
| } |
| return rc; |
| } |
| |
| private void processLdapUser(String password, Long domainId, UserAccount user, Pair<Boolean, ActionOnFailedAuthentication> rc, LdapUser ldapUser, short accountType) { |
| if(!ldapUser.isDisabled()) { |
| rc.first(_ldapManager.canAuthenticate(ldapUser.getPrincipal(), password, domainId)); |
| if(rc.first()) { |
| if(user == null) { |
| // import user to cloudstack |
| createCloudStackUserAccount(ldapUser, domainId, accountType); |
| } else { |
| enableUserInCloudStack(user); |
| } |
| } else if(user != null) { |
| rc.second(ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT); |
| } |
| } else { |
| //disable user in cloudstack |
| disableUserInCloudStack(user); |
| } |
| } |
| |
| /** |
| * checks if the user is configured both in ldap and in cloudstack. |
| * @param username login id |
| * @param password pass phrase |
| * @param domainId domain the user is trying to log on to |
| * @param user cloudstack user object |
| * @return false if either user object does not exist or authenitication fails |
| */ |
| Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, UserAccount user) { |
| boolean result = false; |
| |
| if(user != null ) { |
| try { |
| LdapUser ldapUser = _ldapManager.getUser(username, domainId); |
| if(!ldapUser.isDisabled()) { |
| result = _ldapManager.canAuthenticate(ldapUser.getPrincipal(), password, domainId); |
| } else { |
| s_logger.debug("user with principal "+ ldapUser.getPrincipal() + " is disabled in ldap"); |
| } |
| } catch (NoLdapUserMatchingQueryException e) { |
| s_logger.debug(e.getMessage()); |
| } |
| } |
| return (!result && user != null) ? |
| new Pair<Boolean, ActionOnFailedAuthentication>(result, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT): |
| new Pair<Boolean, ActionOnFailedAuthentication>(result, null); |
| } |
| |
| private void enableUserInCloudStack(UserAccount user) { |
| if(user != null && (user.getState().equalsIgnoreCase(Account.State.disabled.toString()))) { |
| _accountManager.enableUser(user.getId()); |
| } |
| } |
| |
| private void createCloudStackUserAccount(LdapUser user, long domainId, short accountType) { |
| String username = user.getUsername(); |
| _accountManager.createUserAccount(username, "", user.getFirstname(), user.getLastname(), user.getEmail(), null, username, |
| accountType, RoleType.getByAccountType(accountType).getId(), domainId, null, null, |
| UUID.randomUUID().toString(), UUID.randomUUID().toString(), User.Source.LDAP); |
| } |
| |
| private void disableUserInCloudStack(UserAccount user) { |
| if (user != null) { |
| _accountManager.disableUser(user.getId()); |
| } |
| } |
| |
| private void removeUserInCloudStack(UserAccount user) { |
| if (user != null) { |
| _accountManager.disableUser(user.getId()); |
| } |
| } |
| |
| @Override |
| public String encode(final String password) { |
| return password; |
| } |
| } |