All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.graylog2.security.UserSessionTerminationService Maven / Gradle / Ivy

There is a newer version: 6.1.4
Show newest version
/*
 * Copyright (C) 2020 Graylog, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Server Side Public License, version 1,
 * as published by MongoDB, Inc.
 *
 * This program 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
 * Server Side Public License for more details.
 *
 * You should have received a copy of the Server Side Public License
 * along with this program. If not, see
 * .
 */
package org.graylog2.security;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.util.concurrent.AbstractIdleService;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.plugin.database.users.User;
import org.graylog2.shared.users.UserService;
import org.graylog2.users.events.UserChangedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.Serializable;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNull;

/**
 * This service checks on startup if all user sessions should be terminated. This can be requested by bumping the
 * {@code TERMINATION_REVISION} number.
 *
 * The service also listens for {@link UserChangedEvent} events and terminates all sessions for disabled users.
 */
@Singleton
public class UserSessionTerminationService extends AbstractIdleService {
    /**
     * The revision must be bumped if all active user sessions should be terminated on server
     * startup after a Graylog update.
     */
    private static final long TERMINATION_REVISION = 1;

    private static final Logger LOG = LoggerFactory.getLogger(UserSessionTerminationService.class);

    // Sessions should be terminated when a user's account status is one of the following
    // (Note: the DELETED status is not yet used throughout Graylog and won't be set for any user atm.
    // But it will probably be used in the future to mark user accounts as deleted instead of purging them from the DB.)
    private static final EnumSet SESSION_TERMINATION_STATUS = EnumSet.of(
            User.AccountStatus.DELETED,
            User.AccountStatus.DISABLED
    );

    private final MongoDbSessionDAO sessionDao;
    private final MongoDBSessionService sessionService;
    private final DefaultSecurityManager securityManager;
    private final ClusterConfigService clusterConfigService;
    private final UserService userService;
    private final EventBus eventBus;

    @Inject
    public UserSessionTerminationService(MongoDbSessionDAO sessionDao,
                                         MongoDBSessionService sessionService,
                                         DefaultSecurityManager securityManager,
                                         ClusterConfigService clusterConfigService,
                                         UserService userService,
                                         EventBus eventBus) {
        this.sessionDao = sessionDao;
        this.securityManager = securityManager;
        this.clusterConfigService = clusterConfigService;
        this.sessionService = sessionService;
        this.userService = userService;
        this.eventBus = eventBus;
    }

    // Listens on the event bus to terminate users sessions when a user gets disabled or deleted.
    // The UserChangedEvent is only published on the local EventBus and not the cluster EventBus so every node
    // should react to change events.
    @Subscribe
    public void handleUserChanged(UserChangedEvent event) {
        final User user = userService.loadById(event.userId());

        if (user != null && SESSION_TERMINATION_STATUS.contains(user.getAccountStatus())) {
            terminateSessionsForUser(user);
        }
    }

    @Override
    protected void startUp() throws Exception {
        eventBus.register(this);
    }

    @Override
    protected void shutDown() throws Exception {
        eventBus.unregister(this);
    }

    /**
     * Terminate all user sessions, if the revision is outdated.
     * 

* To make sure that we don't terminate user sessions concurrently, this method should be called on the primary * node only. */ public void runGlobalSessionTermination() { final GlobalTerminationRevisionConfig globalTerminationRevisionConfig = clusterConfigService.getOrDefault( GlobalTerminationRevisionConfig.class, GlobalTerminationRevisionConfig.initial() ); if (!globalTerminationRevisionConfig.isOutdated()) { LOG.debug("Global session termination not required"); return; } long terminatedSessions = 0; for (final Session activeSession : sessionDao.getActiveSessions()) { terminateSessionForID(activeSession.getId()); terminatedSessions++; } LOG.info("Globally terminated {} session(s)", terminatedSessions); clusterConfigService.write(GlobalTerminationRevisionConfig.withCurrentRevision()); } public List getActiveSessionsForUser(User user) { return getSessionIDsForUser(user).stream().map(this::getActiveSessionForID).flatMap(Optional::stream).toList(); } private void terminateSessionsForUser(User user) { try { for (final Session session : getActiveSessionsForUser(user)) { LOG.info("Terminating session for user <{}/{}>", user.getName(), user.getId()); session.stop(); } } catch (Exception e) { LOG.error("Couldn't terminate session for user <{}/{}>", user.getName(), user.getId(), e); } } private void terminateSessionForID(Serializable sessionId) { try { getActiveSessionForID(sessionId).ifPresent(Session::stop); } catch (Exception e) { LOG.error("Couldn't terminate session", e); } } private Optional getActiveSessionForID(Serializable sessionId) { final SessionManager sessionManager = securityManager.getSessionManager(); // Using the session manager to get the session instead of getting it directly from the SessionDAO // because the session manager wraps it in a DelegatingSession that might do additional cleanup. try { return Optional.ofNullable(sessionManager.getSession(new DefaultSessionKey(sessionId))); } catch (ExpiredSessionException e) { return Optional.empty(); } } private Set getSessionIDsForUser(User user) { final String userId = requireNonNull(user.getId(), "user ID cannot be null"); return sessionService.loadAll().stream() .filter(session -> userId.equals(session.getUserIdAttribute().orElse(null))) .map(MongoDbSession::getSessionId) .collect(Collectors.toSet()); } @AutoValue public static abstract class GlobalTerminationRevisionConfig { @JsonProperty("applied_revision") public abstract long appliedRevision(); public static GlobalTerminationRevisionConfig initial() { // Default to 0 to make sure sessions get cleaned up on first run return create(0); } public static GlobalTerminationRevisionConfig withCurrentRevision() { return create(TERMINATION_REVISION); } @JsonIgnore public boolean isOutdated() { return appliedRevision() < TERMINATION_REVISION; } @JsonCreator public static GlobalTerminationRevisionConfig create(@JsonProperty("applied_revision") long appliedRevision) { return new AutoValue_UserSessionTerminationService_GlobalTerminationRevisionConfig(appliedRevision); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy