/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package framework.beans.security;
import framework.beans.security.passwords.SessionPassword;
import framework.beans.security.passwords.PasswordEncryptor;
import framework.beans.FindEntity;
import framework.beans.FindEntity.Field;
import framework.beans.config.server.ConfigParametrAbstract;
import framework.beans.config.server.ServConfig;
import framework.beans.security.entities.CollaboratorRightAbstract;
import framework.beans.security.entities.CollaboratorSessionActive;
import framework.beans.collaborator.CollaboratorAbstract;
import framework.generic.ClipsServerConstants;
import framework.generic.ClipsServerException;
import framework.generic.ESecurity;
import java.util.Date;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.ejb.Stateful;
import framework.security.UserRightsSetAbstract;
import java.util.Hashtable;
import javax.naming.AuthenticationException;
import javax.naming.AuthenticationNotSupportedException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
/**
* С подключения к этому бину начинается работа любого клиента. В ответ на авторизацию,
* если всё пройдёт успешно, бин выдаёт идентификатор сессии клиента. И сохранает этот номер в
* таблице активных сессий и у себя.
*
* Клиент должен тормошить сервер с помошью вызова метода DisturbServer() каждые
* ClipsServerConstants.DISTURB_SERVER_PERIOD_MS миллисекунд, чтобы сервер не посчитал, что
* сессия перестала быть активной.
*
* После прекращения работы клиент может вызвать метод logout, чтобы вычеркнуть сессию из
* списка активных немедленно. Если этого не сделать, это будет сделанно автоматически, но позже.
*
* Локальный интерфейс бина предназначен для того, что бы остальные бины могли узнать права
* пользователя по любой указанной сессии. Для использования локального интерфейса нет надобности
* авторизировать бин.
*
* Для ускорения работы бин кеширует права активных сессий. Удаление старых сессий происходит
* перед обращением к бину либо через метод login либо через getSesssionData или getMySessionData,
* но не чаще чем через определённый отрезок времени ClipsServerConstants.AUTO_LOGOUT_PERIOD_MS.
*
* @author antony
*/
public abstract class LoginBeanAbstarct extends FindEntity implements LoginRemoteAbstarct, LoginLocal {
public static char [] DEFAULT_ADMIN_PASSWORD = "314159".toCharArray();
private static final HashMap<Integer, SessionSecurityDetails> sessionDataCash = new HashMap<Integer, SessionSecurityDetails>(256, 0.5f);
private static long lastCleanTime = 0;
protected SessionPassword sessionPassword;
private boolean useLDAP = true;
private void cleanOldSessions() throws ClipsServerException {
long curTime = System.currentTimeMillis();
if (curTime - lastCleanTime <= ClipsServerConstants.AUTO_LOGOUT_PERIOD_MS) {
return;
}
try {
System.out.println("Clean old sessions: " + new Date(curTime));
Date d = new Date(curTime - ClipsServerConstants.AUTO_LOGOUT_PERIOD_MS);
Field f[] = {new Field("lastCallMoment", d, Field.OPERATOR_EQUAL_OR_LESS)};
deleteEntityList(CollaboratorSessionActive.class, f);
manager.flush();
} catch (ClipsServerException ex) {
throw ex;
} catch (Throwable ex) {
throw new ClipsServerException("Внутренняя ошибка при очистке таблицы CollaboratorSessionActive (ошибка не критичная, только возникать будет периодически)", ex);
}
synchronized (sessionDataCash) {
Iterator<Integer> it = sessionDataCash.keySet().iterator();
while (it.hasNext()) {
Integer e = it.next();
if (manager.find(CollaboratorSessionActive.class, e) == null) {
it.remove();
}
}
lastCleanTime = curTime;
}
}
/**
* Подготовка к регистарции пользователя в системе.
* Выдаёт случайные данные, с которой на клиенте следует хешировать хеш пароля.
* @return - случа
*/
@Override
public PasswordEncryptor getEncryptor() {
if(sessionPassword == null) {
ServConfig sc = manager.find(ServConfig.class,
ConfigParametrAbstract.ID_LDAP_USE);
String use = (sc == null) ? "false" : sc.getStrvalue();
useLDAP = Boolean.parseBoolean(use);
sessionPassword = new SessionPassword(useLDAP);
}
return sessionPassword.getEncryptor();
}
/**
* Удостоверяет подлинность пароля пользователя и регистрирует пользователя в системе как активного.
* Обязательно вызывать этот метод перед началом работы клиента.
* @param aCollaboratorID
* @param tryPasswdHash - хеш просоленного хеша пароля, т.е. ХЕШ (ХЕШ(пароля), salt)
* где salt - результат, полученный функцией queryLogin
* @return Возвращает идентификатор сессии пользователя - случайное число.
* @throws Exception
* @throws ClipsServerException
*/
@Override
public int login(Object aCollaboratorID, byte[] tryPasswdHash) throws Exception, ClipsServerException {
cleanOldSessions();
CollaboratorAbstract colEntity = findEntity(CollaboratorAbstract.class, (Integer) aCollaboratorID, "сотрудники");
if (sessionPassword == null) {
throw new ESecurity("Внутренняя ошибка: Перед вызовом login должен быть вызван getEncryptor ");
}
//check is admin or not
ServConfig sc = manager.find(ServConfig.class, ConfigParametrAbstract.ID_ADMIN_PASSWORD_HASH);
String adminPassword = (sc == null) ? null : sc.getStrvalue();
boolean isSuperUser = false;
if (adminPassword != null && !adminPassword.isEmpty()) {
isSuperUser = sessionPassword.verifyHash(tryPasswdHash,
SessionPassword.char2byte(adminPassword.toCharArray()));
} else {
isSuperUser = sessionPassword.verifyPassword(tryPasswdHash, DEFAULT_ADMIN_PASSWORD);
}
if (!isSuperUser) {
if(useLDAP) {
checkLDAP(colEntity.getLdapName(), tryPasswdHash);
} else {
if (colEntity.getPasswordHash() == null) {
throw new ESecurity("У Вас не установлен пароль, поэтому вход в систему невозможен. Обратитесь к администратору");
}
if (!sessionPassword.verifyHash(tryPasswdHash, colEntity.getPasswordHash())) {
sessionPassword = null;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// DO NOTHING
}
throw new ESecurity("Введён неверный пароль");
}
}
}
sessionPassword = null;
return registerSesion(colEntity, isSuperUser);
}
protected int registerSesion(CollaboratorAbstract colEntity, boolean isSuperUser) throws ClipsServerException{
CollaboratorSessionActive ses;
Throwable ex = null;
Random rnd = new Random();
int sd = 0, newSessionId = 0, i = 0;
//@todo иногда 10 не хватает
while (newSessionId == 0 && i < ClipsServerConstants.MAX_LOGIN_ATTEMPT_COUNT) {
do {
sd = rnd.nextInt(Integer.MAX_VALUE) + 1;
ses = manager.find(CollaboratorSessionActive.class, sd);
} while (ses != null);
ses = new CollaboratorSessionActive();
ses.setSessionId(sd);
ses.setCollaborator(colEntity);
ses.setLastCallMoment(new Date());
ses.setAdmin(isSuperUser);
ses.incRefCount();
try {
manager.persist(ses);
manager.flush();
newSessionId = sd;
} catch (Throwable e) {
ex = e;
}
i++;
}
if (newSessionId == 0) {
throw new ClipsServerException("Внутренняя ошибка: невозможно зарегистрировать сессию на сервере", ex);
}
return newSessionId;
}
/**
*
* @param ldapName
* @param tryPasswdHash
*/
private void checkLDAP(String ldapName, byte[] tryPasswdHash) throws ClipsServerException {
Hashtable<String, String> env = new Hashtable<String, String>();
ServConfig sc = manager.find(ServConfig.class, ConfigParametrAbstract.ID_LDAP_URL);
String url = (sc == null) ? "" : sc.getStrvalue();
ServConfig scSSL = manager.find(ServConfig.class, ConfigParametrAbstract.ID_LDAP_USE_SSL);
Boolean ssl = (scSSL == null) ? false : Boolean.parseBoolean(scSSL.getStrvalue());
ServConfig scMask = manager.find(ServConfig.class, ConfigParametrAbstract.ID_LDAP_SEARCH_MASK);
String mask = (scMask == null) ? "$1" : scMask.getStrvalue();
mask = mask.replaceAll("\\$1", "%s");
mask = String.format(mask, ldapName);
ServConfig scType = manager.find(ServConfig.class, ConfigParametrAbstract.ID_LDAP_CRYPTO_SCHEME);
String crypt = (scType == null) ? "simple" : scType.getStrvalue();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
env.put(Context.SECURITY_AUTHENTICATION, crypt);
env.put(Context.SECURITY_PRINCIPAL, mask);
if(ssl) {
env.put(Context.SECURITY_PROTOCOL, "ssl");
}
String passwd = new String(sessionPassword.decryptPassword(tryPasswdHash));
env.put(Context.SECURITY_CREDENTIALS, passwd);
try {
DirContext ctx = new InitialDirContext(env);
ctx.close();
} catch (AuthenticationNotSupportedException ex) {
String str = "Указанная схема аутентификации не поддерживается сервером LDAP.";
env.put(Context.SECURITY_AUTHENTICATION, "none");
env.remove(Context.SECURITY_PRINCIPAL);
env.remove(Context.SECURITY_CREDENTIALS);
try {
DirContext ctx = new InitialDirContext(env);
Attributes attrs = ctx.getAttributes("ldap://localhost:389", new String[]{"supportedSASLMechanisms"});
NamingEnumeration<? extends Attribute> modes = attrs.getAll();
str += "\nПоддерживаемые механизмы SASL: ";
while(modes.hasMore()) {
Attribute attr = modes.next();
for(int i=0; i<attr.size(); i++) {
str += attr.get(i) + (i+1< attr.size() ? ", ":"");
}
}
ctx.close();
} catch (Exception e) {
e.printStackTrace();
//do nothing
}
throw new ClipsServerException(str);
} catch(AuthenticationException ex) {
throw new ClipsServerException("Указан неверный LDAP логин или пароль.");
} catch (Exception ex) {
throw new ClipsServerException("Ошибка при попытке аутентификации.", ex);
}
}
/**
* Вычёркивает сессию из списка активных. Этот метод желательно вызвать по окончании сессии
* клиента. Но можно и не вызывать.
* @param aSessionId
* @throws ClipsServerException
*/
@Override
public void logout(int aSessionId) throws ClipsServerException {
CollaboratorSessionActive ses = null;
synchronized (sessionDataCash) {
if (aSessionId != 0) {
if (sessionDataCash.containsKey(aSessionId)) {
sessionDataCash.remove(aSessionId);
}
ses = manager.find(CollaboratorSessionActive.class, aSessionId);
}
}
if (ses == null) {
throw new ClipsServerException("Внутренняя ошибка: Попытка завершить незарегистрированную сессию");
}
ses.decRefCount();
if (ses.getRefCount() == 0) {
manager.remove(ses);
} else {
manager.merge(ses);
}
}
/**
* Выдаёт информацию о сессии клиента в основном - просто права. Сначала информация ищется
* в кеше, а потом, если не находится, в таблице активных сессий и в таблице прав пользователей.
* Если не будет найдена, значит - ошибочный вызов метода, нет такой сессии.
* Метод доступен только через локальный интерфейс. Будет работать и при не зарегистрированном
* с помощью login бине.
* @param aSessionId - идентификатор сессии по которой следует узнать права.
* @return
* @throws ClipsServerException
*/
@Override
public SessionSecurityDetails getSession(int aSessionId) throws ClipsServerException {
cleanOldSessions();
if (aSessionId == 0) {
throw new ESecurity("Внутренняя ошибка: Пользователь не зарегистрирован. Идентификатор сессии клиента = 0");
}
SessionSecurityDetails d;
synchronized (sessionDataCash) {
d = sessionDataCash.get(aSessionId);
if (d == null) {
CollaboratorSessionActive ses = manager.find(CollaboratorSessionActive.class, aSessionId);
if (ses == null) {
throw new ESecurity("Внутренняя ошибка: Указана несуществующая сессия клиента");
}
d = new SessionSecurityDetails();
d.sessionId = ses.getSessionId();
d.collaboratorId = ses.getCollaborator().getId();
d.isSuperUser = ses.isSuperUser();
d.currentUserRights = getCollaboratorRights(d.collaboratorId, d.isSuperUser);
sessionDataCash.put(aSessionId, d);
}
}
return d;
}
@Override
public boolean hasSession(int sessionID) {
synchronized (sessionDataCash) {
return sessionDataCash.containsKey(sessionID);
}
}
/**
* Вы даёт информацию, о сессии. Доступен через удалённый интерфейс.
* Почему то бинс ругается (немного) если один и тот же метод есть в локальном и удалённом
* интерфейсах. Может и ерунда, но я добавил ещё этот.
* @param aSessionId
* @return
* @throws ClipsServerException
*/
@Override
public SessionSecurityDetails getSessionRemote(int aSessionId) throws ClipsServerException {
//OLD return (SessionSecurityData) ((SessionSecurityDataImp) getSessionData(aSessionId)).clone();
return getSession(aSessionId);
}
private BitSet getCollaboratorRights(int aCollaboratorId, boolean isSuperUser) {
BitSet bits = new BitSet(UserRightsSetAbstract.ApproxMaxRightsCount);
if (! isSuperUser) {
List<CollaboratorRightAbstract> rt = findEntityList(CollaboratorRightAbstract.class, "id.collId", aCollaboratorId);
for (CollaboratorRightAbstract w : rt) {
bits.set(((CollaboratorRightAbstract) w).getId().getRightId());
}
} else {
bits.set(0, UserRightsSetAbstract.ApproxMaxRightsCount);
// Iterator<UserRight> it = UserRightsSetAbstract.elements().iterator();
// while (it.hasNext()) {
// bits.set(it.next().getID());
// }
}
return bits;
}
/**
* "Тормошит" сервер, чтобы он не завершал сессию.
* @param aSessionId
* @throws ESecurity
*/
@Override
public void disturbServer(int aSessionId) throws ESecurity {
CollaboratorSessionActive ses = null;
if (aSessionId != 0) {
ses = manager.find(CollaboratorSessionActive.class, aSessionId);
}
if (ses == null) {
throw new ESecurity("Внутренняя ошибка: Попытка обновить незарегистрированную сессию");
}
ses.setLastCallMoment(new Date());
manager.merge(ses);
}
}