/** * This file is part of Graylog. * * Graylog is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Graylog is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Graylog. If not, see <http://www.gnu.org/licenses/>. */ package org.graylog2.security; import com.github.rholder.retry.RetryException; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; import com.github.rholder.retry.WaitStrategies; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.mongodb.DuplicateKeyException; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.SimpleSession; import org.apache.shiro.session.mgt.eis.CachingSessionDAO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class MongoDbSessionDAO extends CachingSessionDAO { private static final Logger LOG = LoggerFactory.getLogger(MongoDbSessionDAO.class); private final MongoDBSessionService mongoDBSessionService; @Inject public MongoDbSessionDAO(MongoDBSessionService mongoDBSessionService) { this.mongoDBSessionService = mongoDBSessionService; } @Override protected Serializable doCreate(Session session) { final Serializable id = generateSessionId(session); assignSessionId(session, id); Map<String, Object> fields = Maps.newHashMap(); fields.put("session_id", id); fields.put("host", session.getHost()); fields.put("start_timestamp", session.getStartTimestamp()); fields.put("last_access_time", session.getLastAccessTime()); fields.put("timeout", session.getTimeout()); Map<String, Object> attributes = Maps.newHashMap(); for (Object key : session.getAttributeKeys()) { attributes.put(key.toString(), session.getAttribute(key)); } fields.put("attributes", attributes); final MongoDbSession dbSession = new MongoDbSession(fields); final String objectId = mongoDBSessionService.saveWithoutValidation(dbSession); LOG.debug("Created session {}", objectId); return id; } @Override protected Session doReadSession(Serializable sessionId) { final MongoDbSession dbSession = mongoDBSessionService.load(sessionId.toString()); LOG.debug("Reading session for id {} from MongoDB: {}", sessionId, dbSession); if (dbSession == null) { // expired session or it was never there to begin with return null; } return getSimpleSession(sessionId, dbSession); } private SimpleSession getSimpleSession(Serializable sessionId, MongoDbSession dbSession) { final SimpleSession session = new SimpleSession(); assignSessionId(session, sessionId); session.setHost(dbSession.getHost()); session.setTimeout(dbSession.getTimeout()); session.setStartTimestamp(dbSession.getStartTimestamp()); session.setLastAccessTime(dbSession.getLastAccessTime()); session.setExpired(dbSession.isExpired()); session.setAttributes(dbSession.getAttributes()); return session; } @Override protected void doUpdate(Session session) { final MongoDbSession dbSession = mongoDBSessionService.load(session.getId().toString()); if (null == dbSession) { throw new RuntimeException("Couldn't load session <" + session.getId() + ">"); } LOG.debug("Updating session {}", session); dbSession.setHost(session.getHost()); dbSession.setTimeout(session.getTimeout()); dbSession.setStartTimestamp(session.getStartTimestamp()); dbSession.setLastAccessTime(session.getLastAccessTime()); if (session instanceof SimpleSession) { final SimpleSession simpleSession = (SimpleSession) session; dbSession.setAttributes(simpleSession.getAttributes()); dbSession.setExpired(simpleSession.isExpired()); } else { throw new RuntimeException("Unsupported session type: " + session.getClass().getCanonicalName()); } // Due to https://jira.mongodb.org/browse/SERVER-14322 upserts can fail under concurrency. // We need to retry the update, and stagger them a bit, so no all of the retries attempt it at the same time again. // Usually this should succeed the first time, though final Retryer<Object> retryer = RetryerBuilder.newBuilder() .retryIfExceptionOfType(DuplicateKeyException.class) .withWaitStrategy(WaitStrategies.randomWait(5, TimeUnit.MILLISECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(10)) .build(); try { retryer.call(() -> mongoDBSessionService.saveWithoutValidation(dbSession)); } catch (ExecutionException e) { LOG.warn("Unexpected exception when saving session to MongoDB. Failed to update session.", e); throw new RuntimeException(e.getCause()); } catch (RetryException e) { LOG.warn("Tried to update session 10 times, but still failed. This is likely because of https://jira.mongodb.org/browse/SERVER-14322", e); throw new RuntimeException(e.getCause()); } } @Override protected void doDelete(Session session) { LOG.debug("Deleting session {}", session); final Serializable id = session.getId(); final MongoDbSession dbSession = mongoDBSessionService.load(id.toString()); if (dbSession != null) { final int deleted = mongoDBSessionService.destroy(dbSession); LOG.debug("Deleted {} sessions with ID {} from database", deleted, id); } else { LOG.debug("Session {} not found in database", id); } } @Override public Collection<Session> getActiveSessions() { LOG.debug("Retrieving all active sessions."); Collection<MongoDbSession> dbSessions = mongoDBSessionService.loadAll(); List<Session> sessions = Lists.newArrayList(); for (MongoDbSession dbSession : dbSessions) { sessions.add(getSimpleSession(dbSession.getSessionId(), dbSession)); } return sessions; } }