package org.zstack.ldap;
import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.transaction.annotation.Transactional;
import org.zstack.core.Platform;
import org.zstack.core.cloudbus.CloudBus;
import org.zstack.core.cloudbus.MessageSafe;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.db.SimpleQuery;
import org.zstack.core.errorcode.ErrorFacade;
import org.zstack.header.AbstractService;
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.header.identity.*;
import org.zstack.header.message.APIMessage;
import org.zstack.header.message.Message;
import org.zstack.identity.AccountManager;
import org.zstack.identity.IdentityGlobalConfig;
import org.zstack.tag.TagManager;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
import javax.persistence.Query;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by miao on 16-9-6.
*/
public class LdapManagerImpl extends AbstractService implements LdapManager {
private static final CLogger logger = Utils.getLogger(LdapManagerImpl.class);
@Autowired
private DatabaseFacade dbf;
@Autowired
private AccountManager acntMgr;
@Autowired
private TagManager tagMgr;
@Autowired
private CloudBus bus;
@Autowired
private ErrorFacade errf;
@Transactional(readOnly = true)
private LdapServerVO getLdapServer() {
SimpleQuery<LdapServerVO> sq = dbf.createQuery(LdapServerVO.class);
List<LdapServerVO> ldapServers = sq.list();
if (ldapServers.isEmpty()) {
throw new CloudRuntimeException("No ldap server record in database.");
}
if (ldapServers.size() > 1) {
throw new CloudRuntimeException("More than one ldap server record in database.");
}
return ldapServers.get(0);
}
private LdapTemplateContextSource readLdapServerConfiguration() {
LdapServerVO ldapServerVO = getLdapServer();
LdapServerInventory ldapServerInventory = LdapServerInventory.valueOf(ldapServerVO);
return new LdapUtil().loadLdap(ldapServerInventory);
}
@MessageSafe
public void handleMessage(Message msg) {
if (msg instanceof APIMessage) {
handleApiMessage((APIMessage) msg);
} else {
handleLocalMessage(msg);
}
}
private void handleLocalMessage(Message msg) {
bus.dealWithUnknownMessage(msg);
}
private void handleApiMessage(APIMessage msg) {
if (msg instanceof APILogInByLdapMsg) {
handle((APILogInByLdapMsg) msg);
} else if (msg instanceof APIAddLdapServerMsg) {
handle((APIAddLdapServerMsg) msg);
} else if (msg instanceof APIDeleteLdapServerMsg) {
handle((APIDeleteLdapServerMsg) msg);
} else if (msg instanceof APICreateLdapBindingMsg) {
handle((APICreateLdapBindingMsg) msg);
} else if (msg instanceof APIDeleteLdapBindingMsg) {
handle((APIDeleteLdapBindingMsg) msg);
} else if (msg instanceof APIUpdateLdapServerMsg) {
handle((APIUpdateLdapServerMsg) msg);
} else if (msg instanceof APICleanInvalidLdapBindingMsg) {
handle((APICleanInvalidLdapBindingMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
public boolean isValid(String uid, String password) {
LdapTemplateContextSource ldapTemplateContextSource = readLdapServerConfiguration();
try {
boolean valid;
String fullUserDn = getFullUserDn(ldapTemplateContextSource.getLdapTemplate(), "uid", uid);
if (fullUserDn.equals("") || password.equals("")) {
return false;
}
LdapServerVO ldapServerVO = getLdapServer();
LdapServerInventory ldapServerInventory = LdapServerInventory.valueOf(ldapServerVO);
ldapServerInventory.setUsername(fullUserDn);
ldapServerInventory.setPassword(password);
LdapTemplateContextSource ldapTemplateContextSource2 = new LdapUtil().loadLdap(ldapServerInventory);
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter(fullUserDn.split(",")[0].split("=")[0], fullUserDn.split(",")[0].split("=")[1]));
valid = ldapTemplateContextSource2.getLdapTemplate().
authenticate("", filter.toString(), password);
logger.info(String.format("isValid[userName:%s, dn:%s, valid:%s]", uid, fullUserDn, valid));
return valid;
} catch (NamingException e) {
logger.info("isValid fail userName:" + uid, e);
return false;
} catch (Exception e) {
logger.info("isValid error userName:" + uid, e);
return false;
}
}
@Transactional
private LdapAccountRefInventory bindLdapAccount(String accountUuid, String ldapUid) {
LdapAccountRefVO ref = new LdapAccountRefVO();
ref.setUuid(Platform.getUuid());
ref.setAccountUuid(accountUuid);
ref.setLdapServerUuid(getLdapServer().getUuid());
ref.setLdapUid(ldapUid);
ref = dbf.persistAndRefresh(ref);
return LdapAccountRefInventory.valueOf(ref);
}
private String getPartialUserDnByUid(LdapTemplateContextSource ldapTemplateContextSource, String uid) {
return getFullUserDn(ldapTemplateContextSource.getLdapTemplate(), "uid", uid).
replace("," + ldapTemplateContextSource.getLdapContextSource().getBaseLdapPathAsString(), "");
}
private String getFullUserDn(LdapTemplate ldapTemplate, String key, String val) {
String dn;
try {
EqualsFilter f = new EqualsFilter(key, val);
List<Object> result = ldapTemplate.search("", f.toString(), new AbstractContextMapper<Object>() {
@Override
protected Object doMapFromContext(DirContextOperations ctx) {
return ctx.getNameInNamespace();
}
});
if (result.size() == 1) {
dn = result.get(0).toString();
} else if (result.size() > 1) {
throw new OperationFailureException(errf.instantiateErrorCode(
LdapErrors.UNABLE_TO_GET_SPECIFIED_LDAP_UID, "More than one ldap search result"));
} else {
return "";
}
logger.info(String.format("getDn success key:%s, val:%s, dn:%s", key, val, dn));
} catch (NamingException e) {
LdapServerVO ldapServerVO = getLdapServer();
String errString = String.format(
"You'd better check the ldap server[url:%s, baseDN:%s, encryption:%s, username:%s, password:******]" +
" configuration and test connection first.getDn error key:%s, val:%s",
ldapServerVO.getUrl(), ldapServerVO.getBase(),
ldapServerVO.getEncryption(), ldapServerVO.getUsername(), key, val);
throw new OperationFailureException(errf.instantiateErrorCode(
LdapErrors.UNABLE_TO_GET_SPECIFIED_LDAP_UID, errString));
}
return dn;
}
public String getId() {
return bus.makeLocalServiceId(LdapConstant.SERVICE_ID);
}
private SessionInventory getSession(String accountUuid, String userUuid) {
int maxLoginTimes = org.zstack.identity.IdentityGlobalConfig.MAX_CONCURRENT_SESSION.value(Integer.class);
SimpleQuery<SessionVO> query = dbf.createQuery(SessionVO.class);
query.add(SessionVO_.accountUuid, SimpleQuery.Op.EQ, accountUuid);
query.add(SessionVO_.userUuid, SimpleQuery.Op.EQ, userUuid);
long count = query.count();
if (count >= maxLoginTimes) {
String err = String.format("Login sessions hit limit of max allowed concurrent login sessions, max allowed: %s", maxLoginTimes);
throw new BadCredentialsException(err);
}
int sessionTimeout = IdentityGlobalConfig.SESSION_TIMEOUT.value(Integer.class);
SessionVO svo = new SessionVO();
svo.setUuid(Platform.getUuid());
svo.setAccountUuid(accountUuid);
svo.setUserUuid(userUuid);
long expiredTime = getCurrentSqlDate().getTime() + TimeUnit.SECONDS.toMillis(sessionTimeout);
svo.setExpiredDate(new Timestamp(expiredTime));
svo = dbf.persistAndRefresh(svo);
SessionInventory session = SessionInventory.valueOf(svo);
return session;
}
@Transactional(readOnly = true)
private Timestamp getCurrentSqlDate() {
Query query = dbf.getEntityManager().createNativeQuery("select current_timestamp()");
return (Timestamp) query.getSingleResult();
}
public boolean start() {
return true;
}
public boolean stop() {
return true;
}
private void handle(APILogInByLdapMsg msg) {
APILogInByLdapReply reply = new APILogInByLdapReply();
SimpleQuery<LdapAccountRefVO> q = dbf.createQuery(LdapAccountRefVO.class);
q.add(LdapAccountRefVO_.ldapUid, SimpleQuery.Op.EQ, msg.getUid());
LdapAccountRefVO vo = q.find();
if (vo == null) {
reply.setError(errf.instantiateErrorCode(IdentityErrors.AUTHENTICATION_ERROR,
"No LdapAccountRef Exist."));
bus.reply(msg, reply);
return;
}
if (isValid(vo.getLdapUid(), msg.getPassword())) {
reply.setInventory(getSession(vo.getAccountUuid(), vo.getAccountUuid()));
SimpleQuery<AccountVO> sq = dbf.createQuery(AccountVO.class);
sq.add(AccountVO_.uuid, SimpleQuery.Op.EQ, vo.getAccountUuid());
AccountVO avo = sq.find();
if (avo == null) {
throw new CloudRuntimeException(String.format("Account[uuid:%s] Not Found!!!", vo.getAccountUuid()));
}
reply.setAccountInventory(AccountInventory.valueOf(avo));
} else {
reply.setError(errf.instantiateErrorCode(IdentityErrors.AUTHENTICATION_ERROR,
"Login Failed."));
}
bus.reply(msg, reply);
}
private void handle(APIAddLdapServerMsg msg) {
APIAddLdapServerEvent evt = new APIAddLdapServerEvent(msg.getId());
SimpleQuery<LdapServerVO> sq = dbf.createQuery(LdapServerVO.class);
List<LdapServerVO> ldapServers = sq.list();
if (ldapServers.isEmpty()) {
LdapServerVO ldapServerVO = new LdapServerVO();
ldapServerVO.setUuid(Platform.getUuid());
ldapServerVO.setName(msg.getName());
ldapServerVO.setDescription(msg.getDescription());
ldapServerVO.setUrl(msg.getUrl());
ldapServerVO.setBase(msg.getBase());
ldapServerVO.setUsername(msg.getUsername());
ldapServerVO.setPassword(msg.getPassword());
ldapServerVO.setEncryption(msg.getEncryption());
ldapServerVO = dbf.persistAndRefresh(ldapServerVO);
LdapServerInventory inv = LdapServerInventory.valueOf(ldapServerVO);
evt.setInventory(inv);
} else {
evt.setError(errf.instantiateErrorCode(LdapErrors.MORE_THAN_ONE_LDAP_SERVER,
"There has been a ldap server record. " +
"You'd better remove it before adding a new one!"));
}
bus.publish(evt);
}
private void handle(APIDeleteLdapServerMsg msg) {
APIDeleteLdapServerEvent evt = new APIDeleteLdapServerEvent(msg.getId());
dbf.removeByPrimaryKey(msg.getUuid(), LdapServerVO.class);
bus.publish(evt);
}
private void handle(APICreateLdapBindingMsg msg) {
APICreateLdapBindingEvent evt = new APICreateLdapBindingEvent(msg.getId());
// account check
SimpleQuery<AccountVO> sq = dbf.createQuery(AccountVO.class);
sq.add(AccountVO_.uuid, SimpleQuery.Op.EQ, msg.getAccountUuid());
AccountVO avo = sq.find();
if (avo == null) {
evt.setError(errf.instantiateErrorCode(LdapErrors.CANNOT_FIND_ACCOUNT,
String.format("cannot find the specified account[uuid:%s]", msg.getAccountUuid())));
bus.publish(evt);
return;
}
if (avo.getType().equals(AccountType.SystemAdmin)) {
evt.setError(errf.instantiateErrorCode(LdapErrors.CANNOT_BIND_ADMIN_ACCOUNT,
"cannot bind ldap uid to admin account."));
bus.publish(evt);
return;
}
// bind op
LdapTemplateContextSource ldapTemplateContextSource = readLdapServerConfiguration();
if (getPartialUserDnByUid(ldapTemplateContextSource, msg.getLdapUid()).equals("")) {
throw new OperationFailureException(errf.instantiateErrorCode(LdapErrors.UNABLE_TO_GET_SPECIFIED_LDAP_UID,
String.format("cannot find uid[%s] on ldap server[Address:%s, BaseDN:%s].", msg.getLdapUid(),
String.join(", ", ldapTemplateContextSource.getLdapContextSource().getUrls()),
ldapTemplateContextSource.getLdapContextSource().getBaseLdapPathAsString())));
}
try {
evt.setInventory(bindLdapAccount(msg.getAccountUuid(), msg.getLdapUid()));
} catch (JpaSystemException e) {
if (e.getRootCause() instanceof MySQLIntegrityConstraintViolationException) {
evt.setError(errf.instantiateErrorCode(LdapErrors.BIND_SAME_LDAP_UID_TO_MULTI_ACCOUNT,
"The ldap uid has been bound to an account. "));
} else {
throw e;
}
}
bus.publish(evt);
}
private void handle(APIDeleteLdapBindingMsg msg) {
APIDeleteLdapBindingEvent evt = new APIDeleteLdapBindingEvent(msg.getId());
dbf.removeByPrimaryKey(msg.getUuid(), LdapAccountRefVO.class);
bus.publish(evt);
}
@Transactional
private void handle(APICleanInvalidLdapBindingMsg msg) {
APICleanInvalidLdapBindingEvent evt = new APICleanInvalidLdapBindingEvent(msg.getId());
ArrayList<String> accountUuidList = new ArrayList<>();
ArrayList<String> ldapAccountRefUuidList = new ArrayList<>();
SimpleQuery<LdapAccountRefVO> sq = dbf.createQuery(LdapAccountRefVO.class);
List<LdapAccountRefVO> refList = sq.list();
if (refList != null && !refList.isEmpty()) {
LdapTemplateContextSource ldapTemplateContextSource = readLdapServerConfiguration();
for (LdapAccountRefVO ldapAccRefVO : refList) {
if (getPartialUserDnByUid(ldapTemplateContextSource, ldapAccRefVO.getLdapUid()).equals("")) {
accountUuidList.add(ldapAccRefVO.getAccountUuid());
ldapAccountRefUuidList.add(ldapAccRefVO.getUuid());
}
}
}
if (!accountUuidList.isEmpty()) {
// remove ldap bindings
dbf.removeByPrimaryKeys(ldapAccountRefUuidList, LdapAccountRefVO.class);
// return accounts of which ldap bindings had been removed
SimpleQuery<AccountVO> sq1 = dbf.createQuery(AccountVO.class);
sq1.add(AccountVO_.uuid, SimpleQuery.Op.IN, accountUuidList);
evt.setInventories(sq1.list());
}
bus.publish(evt);
}
private void handle(APIUpdateLdapServerMsg msg) {
APIUpdateLdapServerEvent evt = new APIUpdateLdapServerEvent(msg.getId());
LdapServerVO ldapServerVO = dbf.findByUuid(msg.getLdapServerUuid(), LdapServerVO.class);
if (ldapServerVO == null) {
evt.setError(errf.instantiateErrorCode(LdapErrors.UNABLE_TO_GET_SPECIFIED_LDAP_SERVER_RECORD,
String.format("Cannot find the specified ldap server[uuid:%s] in database.",
msg.getLdapServerUuid())));
bus.publish(evt);
return;
}
//
if (msg.getName() != null) {
ldapServerVO.setName(msg.getName());
}
if (msg.getDescription() != null) {
ldapServerVO.setDescription(msg.getDescription());
}
if (msg.getUrl() != null) {
ldapServerVO.setUrl(msg.getUrl());
}
if (msg.getBase() != null) {
ldapServerVO.setBase(msg.getBase());
}
if (msg.getUsername() != null) {
ldapServerVO.setUsername(msg.getUsername());
}
if (msg.getPassword() != null) {
ldapServerVO.setPassword(msg.getPassword());
}
if (msg.getEncryption() != null) {
ldapServerVO.setEncryption(msg.getEncryption());
}
ldapServerVO = dbf.updateAndRefresh(ldapServerVO);
evt.setInventory(LdapServerInventory.valueOf(ldapServerVO));
bus.publish(evt);
}
}