/*
* Copyright 2011 Future Systems, Inc.
*
* Licensed 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.krakenapps.dom.api.impl;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Set;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Validate;
import org.krakenapps.api.PrimitiveConverter;
import org.krakenapps.confdb.ConfigTransaction;
import org.krakenapps.confdb.Predicate;
import org.krakenapps.confdb.Predicates;
import org.krakenapps.dom.api.AdminApi;
import org.krakenapps.dom.api.ConfigManager;
import org.krakenapps.dom.api.DOMException;
import org.krakenapps.dom.api.DefaultEntityEventListener;
import org.krakenapps.dom.api.EntityEventListener;
import org.krakenapps.dom.api.LoginCallback;
import org.krakenapps.dom.api.OrganizationApi;
import org.krakenapps.dom.api.OtpApi;
import org.krakenapps.dom.api.ProgramApi;
import org.krakenapps.dom.api.Transaction;
import org.krakenapps.dom.api.UserApi;
import org.krakenapps.dom.model.Admin;
import org.krakenapps.dom.model.Permission;
import org.krakenapps.dom.model.ProgramProfile;
import org.krakenapps.dom.model.User;
import org.krakenapps.msgbus.PushApi;
import org.krakenapps.msgbus.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(name = "dom-admin-api")
@Provides
public class AdminApiImpl implements AdminApi {
private static final Class<User> cls = User.class;
private static final String NOT_FOUND = "admin-not-found";
private static final String LOCKED_ADMIN = "locked-admin";
private final Logger logger = LoggerFactory.getLogger(AdminApiImpl.class.getName());
private EntityEventListener<User> userEventListener = new DefaultEntityEventListener<User>() {
@Override
public void entityRemoving(String domain, User obj, ConfigTransaction xact, Object state) {
Admin admin = getAdmin(domain, obj);
if (admin != null && admin.getRole().getName().equals("master"))
throw new DOMException("cannot-master-unset");
}
};
private EntityEventListener<ProgramProfile> programProfileEventListener = new DefaultEntityEventListener<ProgramProfile>() {
@Override
public void entityRemoving(String domain, ProgramProfile obj, ConfigTransaction xact, Object state) {
List<User> users = new ArrayList<User>();
List<Predicate> preds = new ArrayList<Predicate>();
for (Admin admin : getAdmins(domain)) {
if (admin.getProfile().getName().equals(obj.getName())) {
admin.setProfile(null);
users.add(admin.getUser());
preds.add(Predicates.field("loginName", admin.getUser().getLoginName()));
}
}
Transaction x = Transaction.getInstance(xact);
cfg.updates(x, User.class, preds, users, null);
}
};
@Requires
private ConfigManager cfg;
@Requires
private OrganizationApi orgApi;
@Requires
private ProgramApi programApi;
@Requires
private UserApi userApi;
@Requires
private PushApi pushApi;
@Requires(optional = true, nullable = false)
private OtpApi otpApi;
private Set<LoginCallback> callbacks = new HashSet<LoginCallback>();
private PriorityQueue<LoggedInAdmin> loggedIn = new PriorityQueue<LoggedInAdmin>(11, new LoggedInAdminComparator());
@Validate
public void validate() {
userApi.addEntityEventListener(userEventListener);
programApi.getProgramProfileEventProvider().addEntityEventListener(programProfileEventListener);
}
@Invalidate
public void invalidate() {
if (userApi != null)
userApi.removeEntityEventListener(userEventListener);
if (programApi != null)
programApi.getProgramProfileEventProvider().removeEntityEventListener(programProfileEventListener);
}
@Override
public String getExtensionName() {
return "admin";
}
private Predicate getPred() {
return Predicates.not(Predicates.field("ext/admin", null));
}
private Predicate getPred(String loginName) {
return Predicates.and(getPred(), Predicates.field("loginName", loginName));
}
@Override
public Collection<Admin> getAdmins(String domain) {
Collection<Admin> admins = new ArrayList<Admin>();
for (User user : cfg.all(domain, cls, getPred()))
admins.add(parseAdmin(domain, user));
return admins;
}
@Override
public Admin findAdmin(String domain, String loginName) {
return parseAdmin(domain, cfg.find(domain, cls, getPred(loginName)));
}
@Override
public Admin getAdmin(String domain, String loginName) {
Admin admin = findAdmin(domain, loginName);
if (admin == null)
throw new DOMException(NOT_FOUND);
return admin;
}
@Override
public Admin getAdmin(String domain, User user) {
Object o = user.getExt().get(getExtensionName());
if (o == null)
return null;
if (o instanceof Admin)
return (Admin) o;
else if (o instanceof Map)
return parseAdmin(domain, user);
return null;
}
private Admin parseAdmin(String domain, User user) {
if (user == null)
return null;
Admin admin = PrimitiveConverter.parse(Admin.class, user.getExt().get(getExtensionName()), cfg.getParseCallback(domain));
admin.setUser(user);
return admin;
}
@Override
public void setAdmin(String domain, String requestAdminLoginName, String targetUserLoginName, Admin admin) {
checkPermissionLevel(domain, requestAdminLoginName, admin, "set-admin-permission-denied");
prepare(admin);
User target = userApi.getUser(domain, targetUserLoginName);
if (target.getExt().get(getExtensionName()) != null) {
Admin oldAdmin = parseAdmin(domain, target);
if (oldAdmin != null && !oldAdmin.isEnabled() && admin.isEnabled())
admin.setLoginFailures(0);
}
target.getExt().put(getExtensionName(), admin);
userApi.updateUser(domain, target, false);
}
@Override
public String updateOtpSeed(String domain, String requestAdminLoginName, String targetUserLoginName) {
Admin admin = getAdmin(domain, targetUserLoginName);
String newSeed = createOtpSeed();
admin.setOtpSeed(newSeed);
setAdmin(domain, requestAdminLoginName, targetUserLoginName, admin);
return newSeed;
}
private void prepare(Admin admin) {
if (admin.getLang() == null)
admin.setLang("en");
if (admin.getOtpSeed() == null)
admin.setOtpSeed(createOtpSeed());
}
private String createOtpSeed() {
Random random = new Random();
StringBuilder sb = new StringBuilder();
char[] c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
for (int i = 0; i < 10; i++)
sb.append(c[random.nextInt(c.length)]);
return sb.toString();
}
@Override
public void unsetAdmin(String domain, String requestAdminLoginName, String targetUserLoginName) {
// login name is immutable, only one master account per domain, and
// default name is "admin"
if (targetUserLoginName.equals("admin"))
throw new DOMException("cannot-master-unset");
if (targetUserLoginName.equals(requestAdminLoginName))
throw new DOMException("cannot-remove-requesting-admin");
Admin target = getAdmin(domain, targetUserLoginName);
if (requestAdminLoginName != null)
checkPermissionLevel(domain, requestAdminLoginName, target, "unset-admin-permission-denied");
User user = target.getUser();
user.getExt().remove(getExtensionName());
userApi.updateUser(domain, user, false);
}
private void checkPermissionLevel(String domain, String requestAdminLoginName, Admin admin, String exceptionMessage) {
Admin request = findAdmin(domain, requestAdminLoginName);
if (request == null)
throw new DOMException("request-admin-not-found");
if (request.getRole().getLevel() < admin.getRole().getLevel())
throw new DOMException(exceptionMessage);
}
@Override
public Admin login(Session session, String loginName, String hash, boolean force) {
String domain = session.getOrgDomain();
Admin admin = getAdmin(domain, loginName);
try {
checkAcl(session, admin);
if (!admin.isEnabled()) {
int lockTime = 300;
Object param = orgApi.getOrganizationParameter(domain, "login_lock_time");
if (param != null) {
try {
lockTime = (Integer) param;
} catch (NumberFormatException e) {
}
}
Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, -lockTime);
Date failed = admin.getLastLoginFailedDateTime();
if (failed == null || failed.after(c.getTime()))
throw new DOMException(LOCKED_ADMIN);
}
String password = null;
if (otpApi != null && admin.isUseOtp())
password = Sha1.hash(otpApi.getOtpValue(admin.getOtpSeed()));
else
password = admin.getUser().getPassword();
if (password == null)
throw new DOMException("invalid-password");
if (!hash.equals(Sha1.hash(password + session.getString("nonce")))) {
if (admin.isUseOtp())
throw new DOMException("invalid-otp-password");
else
throw new DOMException("invalid-password");
}
enforcePasswordChange(domain, admin);
Integer maxSession = orgApi.getOrganizationParameter(domain, "max_sessions", Integer.class);
if (maxSession != null) {
if (maxSession > 0) {
if (force) {
while (loggedIn.size() >= maxSession) {
if (loggedIn.peek().level > admin.getRole().getLevel())
throw new DOMException("max-session");
LoggedInAdmin ban = loggedIn.poll();
Map<String, Object> m = new HashMap<String, Object>();
m.put("type", "terminate");
m.put("kick_by", admin.getUser().getLoginName());
pushApi.push(ban.session, "kraken-system-event", m);
long wait = System.currentTimeMillis() + 5000;
while (loggedIn.contains(ban) && System.currentTimeMillis() < wait) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
ban.session.close();
}
} else if (loggedIn.size() >= maxSession) {
LoggedInAdmin peek = loggedIn.peek();
if (peek.level > admin.getRole().getLevel())
throw new DOMException("max-session");
Map<String, Object> m = new HashMap<String, Object>();
m.put("login_name", peek.loginName);
m.put("session_id", peek.session.getGuid());
m.put("ip", peek.session.getRemoteAddress().getHostAddress());
throw new DOMException("max-session", m);
}
}
}
updateLoginFailures(domain, admin, true);
loggedIn.add(new LoggedInAdmin(admin.getRole().getLevel(), new Date(), session, admin.getUser().getLoginName()));
for (LoginCallback callback : callbacks)
callback.onLoginSuccess(admin, session);
return admin;
} catch (DOMException e) {
if (!e.getErrorCode().equals("max-session")) {
updateLoginFailures(domain, admin, false);
for (LoginCallback callback : callbacks) {
if (e.getErrorCode().equals(LOCKED_ADMIN))
callback.onLoginLocked(admin, session);
else
callback.onLoginFailed(admin, session, e);
}
}
throw e;
}
}
private void enforcePasswordChange(String domain, Admin admin) {
logger.trace("kraken dom: last password change [{}]", admin.getUser().getLastPasswordChange());
long interval = 0;
if (admin.getUser().getLastPasswordChange() != null)
interval = new Date().getTime() - admin.getUser().getLastPasswordChange().getTime();
Integer passwordExpiry = (Integer) orgApi.getOrganizationParameter(domain, "dom.user.password_expiry");
if (passwordExpiry == null)
passwordExpiry = 365;
long baseline = passwordExpiry * 86400 * 1000L;
logger.trace("kraken dom: checking expired password interval [{}], last change [{}]", interval, baseline);
if (interval > baseline)
throw new DOMException("expired-password");
}
private void checkAcl(Session session, Admin admin) {
String remote = session.getRemoteAddress().getHostAddress();
boolean failed = false;
String hosts = orgApi.getOrganizationParameter(session.getOrgDomain(), "dom.admin.trust_hosts", String.class);
if (hosts != null && !hosts.isEmpty()) {
String[] tokens = hosts.split(",");
for (String token : tokens) {
token = token.trim();
if (token.equals(remote))
return;
}
// check admin specific config (give change)
failed = true;
}
// check per admin
if (!admin.isUseAcl()) {
if (failed)
throw new DOMException("not-trust-host");
return;
}
for (String host : admin.getTrustHosts()) {
if (host != null && host.equals(remote))
return;
}
throw new DOMException("not-trust-host");
}
private void updateLoginFailures(String domain, Admin admin, boolean success) {
if (success) {
admin.setLastLoginDateTime(new Date());
admin.setLastLoginFailedDateTime(null);
admin.setLoginFailures(0);
admin.setEnabled(true);
} else {
if (admin.isEnabled())
admin.setLastLoginFailedDateTime(new Date());
admin.setLoginFailures(admin.getLoginFailures() + 1);
if (admin.isUseLoginLock() && admin.getLoginFailures() >= admin.getLoginLockCount())
admin.setEnabled(false);
}
String loginName = admin.getUser().getLoginName();
setAdmin(domain, loginName, loginName, admin);
}
@Override
public void logout(Session session) {
if (session.getOrgDomain() != null && session.getAdminLoginName() != null) {
logger.trace("kraken dom: logout [domain: {}, login: {}]", session.getOrgDomain(), session.getAdminLoginName());
Admin admin = getAdmin(session.getOrgDomain(), session.getAdminLoginName());
loggedIn.remove(new LoggedInAdmin(session));
for (LoginCallback callback : callbacks)
callback.onLogout(admin, session);
}
}
private class LoggedInAdmin {
private int level;
private Date loginTime;
private Session session;
private String loginName;
private LoggedInAdmin(Session session) {
this.session = session;
}
private LoggedInAdmin(int level, Date loginTime, Session session, String loginName) {
this.level = level;
this.loginTime = loginTime;
this.session = session;
this.loginName = loginName;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + ((session == null) ? 0 : session.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LoggedInAdmin other = (LoggedInAdmin) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (session == null) {
if (other.session != null)
return false;
} else if (!session.equals(other.session))
return false;
return true;
}
private AdminApiImpl getOuterType() {
return AdminApiImpl.this;
}
}
private class LoggedInAdminComparator implements Comparator<LoggedInAdmin> {
@Override
public int compare(LoggedInAdmin o1, LoggedInAdmin o2) {
if (o1.level != o2.level)
return o1.level - o2.level;
else
return o1.loginTime.compareTo(o2.loginTime);
}
}
@Override
public void registerLoginCallback(LoginCallback callback) {
callbacks.add(callback);
}
@Override
public void unregisterLoginCallback(LoginCallback callback) {
callbacks.remove(callback);
}
@Override
public boolean canManage(String domain, Admin admin, User user) {
String loginName = user.getLoginName();
if (!admin.getRole().getPermissions().contains(new Permission("dom", "user_edit")))
return false;
Object ext = user.getExt().get("admin");
if (ext == null)
return true;
Admin targetAdmin = PrimitiveConverter.parse(Admin.class, ext, cfg.getParseCallback(domain));
if (targetAdmin != null && !loginName.equals(admin.getUser().getLoginName())
&& targetAdmin.getRole().getLevel() >= admin.getRole().getLevel()) {
return false;
}
return true;
}
}