package com.fourspaces.featherdb.auth; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import com.fourspaces.featherdb.FeatherDB; import com.fourspaces.featherdb.backend.BackendException; import com.fourspaces.featherdb.document.Document; import com.fourspaces.featherdb.document.DocumentCreationException; import com.fourspaces.featherdb.document.JSONDocument; import com.fourspaces.featherdb.utils.BCrypt; import com.fourspaces.featherdb.utils.Lock; import com.fourspaces.featherdb.utils.Logger; /** * <p> * This authentication backend does two things for checking a username/password. First, it loads an administrator * (sa) username and password from the properties file. Second, it checks to see if a system document * "_sys/users/username" exists. * <p> * The user document must contain a "password" entry, and _can_ contain a boolean "is_sa" flag. * * @author mbreese * */ public class BasicAuthentication implements Authentication { protected Logger log = Logger.get(Lock.class); protected List<Credentials> credentials = Collections.synchronizedList(new ArrayList<Credentials>()); protected static Random random = new java.util.Random(); protected Thread monitor = null; protected String saUsername; protected String saPasswordHash; protected FeatherDB featherDB; protected int timeout; private boolean allowAnonymous; public BasicAuthentication () { } public void init(FeatherDB featherDB) { this.featherDB = featherDB; if (!featherDB.getBackend().doesDatabaseExist(FeatherDB.USERS_DB)) { try { featherDB.getBackend().addDatabase(FeatherDB.USERS_DB); } catch (BackendException e) { e.printStackTrace(); log.error(e); throw new RuntimeException(e); } } String u = featherDB.getProperty("sa.username"); String p = featherDB.getProperty("sa.password"); this.saUsername=u; this.saPasswordHash=BCrypt.hashpw(p, BCrypt.gensalt()); this.allowAnonymous = featherDB.getProperty("auth.anonymous","false").toLowerCase().equals("true"); this.timeout=Integer.parseInt(featherDB.getProperty("auth.timeout.seconds","300")); monitor = new Thread() { boolean stop = false; @Override public void run() { log.debug("Starting authentication cache monitoring thread"); while (!stop) { List<Credentials> expired = new ArrayList<Credentials>(); for (Credentials cred:credentials) { if (cred.isExpired()) { expired.add(cred); } } for (Credentials cred: expired) { log.debug("Invalidating credentials {} (timeout)", cred.getUsername()); invalidate(cred); } try { Thread.sleep(5000); } catch (InterruptedException e) { stop = true; } } log.debug("Stopping authentication cache monitoring thread"); } @Override public void interrupt() { this.stop = true; super.interrupt(); } }; monitor.start(); } public void shutdown() { monitor.interrupt(); } public Credentials addCredentials(Credentials cred) { log.debug("Adding credentials {} to cache", cred.getUsername()); credentials.add(cred); return cred; } public Credentials getCredentialsFromToken(String token) { for (Credentials cred:credentials) { if (cred.getToken().equals(token)) { if (!cred.isExpired()) { cred.resetTimeout(); return cred; } else { invalidate(cred); } } } return null; } public void invalidate(Credentials cred) { if (credentials.contains(cred)) { credentials.remove(cred); } } public Credentials authenticate(String username, String password) { if (password==null) { password=""; } log.debug("Attempting authentication: {}", username); if (allowAnonymous) { return addCredentials(new SACredentials(username,generateToken(),timeout)); } else if (username.equals(saUsername) && BCrypt.checkpw(password,saPasswordHash)) { return addCredentials(new SACredentials(username,generateToken(),timeout)); } else { JSONDocument userdoc = (JSONDocument) featherDB.getBackend().getDocument(FeatherDB.USERS_DB,username); if (userdoc!=null) { String hashedPassword = (String) userdoc.get("password"); if (hashedPassword == null) { hashedPassword = BCrypt.hashpw("", BCrypt.gensalt()); } if (BCrypt.checkpw(password,hashedPassword)) { return new DocumentCredentials(userdoc,generateToken(),timeout); } else { log.debug("Invalid password for user {}",username); } } else { log.debug("No document for user {} found",username); } } return null; } public synchronized String generateToken() { String token = null; while (token==null || getCredentialsFromToken(token)!=null) { token = Long.toHexString(random.nextLong())+Long.toHexString(random.nextLong()); } return token; } public void addUser(Credentials cred, String username, String password, boolean sa) throws NotAuthorizedException { if (cred.isSA()) { try { JSONDocument userdoc = (JSONDocument) Document.newDocument(featherDB.getBackend(),FeatherDB.USERS_DB,username,cred.getUsername()); userdoc.put("username", username); userdoc.put("is_sa", sa); userdoc.put("password", BCrypt.hashpw(password, BCrypt.gensalt())); featherDB.getBackend().saveDocument(userdoc); } catch (BackendException e) { log.error("Backend exception adding user: {}",e,username); } catch (DocumentCreationException e) { log.error("Backend exception adding user: {}",e,username); } } else { throw new NotAuthorizedException("Only sa users can add new users"); } } public void removeUser(Credentials cred, String username) throws NotAuthorizedException { if (cred.isSA()) { try { featherDB.getBackend().deleteDocument(FeatherDB.USERS_DB,username); } catch (BackendException e) { log.error("Backend exception removing user: {}",e,username); } } else { throw new NotAuthorizedException("Only sa users can remove users"); } } public List<Credentials> getCredentials() { return credentials; } }