Skip to content

Add actuator endpoint for finding and deleting sessions #8342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions spring-boot-actuator-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<optional>true</optional>
</dependency>
<!-- Annotation processing -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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.springframework.boot.actuate.autoconfigure.session;

import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.session.SessionsEndpoint;
import org.springframework.boot.actuate.session.SessionsWebEndpointExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;

/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link SessionsEndpoint}.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@Configuration
@ConditionalOnClass(FindByIndexNameSessionRepository.class)
@AutoConfigureAfter(SessionAutoConfiguration.class)
public class SessionsEndpointAutoConfiguration {

@Bean
@ConditionalOnBean(FindByIndexNameSessionRepository.class)
@ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint
public SessionsEndpoint sessionEndpoint(
FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
return new SessionsEndpoint(sessionRepository);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint
@ConditionalOnBean(SessionsEndpoint.class)
public SessionsWebEndpointExtension sessionsWebEndpointExtension(
SessionsEndpoint sessionsEndpoint) {
return new SessionsWebEndpointExtension(sessionsEndpoint);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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.
*/

/**
* Auto-configuration for actuator Spring Sessions concerns.
*/
package org.springframework.boot.actuate.autoconfigure.session;
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration,
org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.redis.RedisHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.solr.SolrHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.TraceEndpointAutoConfiguration,\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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.springframework.boot.actuate.autoconfigure.session;

import org.junit.Test;

import org.springframework.boot.actuate.session.SessionsEndpoint;
import org.springframework.boot.actuate.session.SessionsWebEndpointExtension;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FindByIndexNameSessionRepository;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

/**
* Tests for {@link SessionsEndpointAutoConfiguration}.
*
* @author Vedran Pavic
*/
public class SessionsEndpointAutoConfigurationTests {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(SessionsEndpointAutoConfiguration.class))
.withUserConfiguration(SessionConfiguration.class);

@Test
public void runShouldHaveEndpointBean() {
this.contextRunner.run(
(context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class));
}

@Test
public void runShouldHaveWebExtensionBean() {
this.contextRunner.run((context) -> assertThat(context)
.hasSingleBean(SessionsWebEndpointExtension.class));
}

@Test
public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointOrExtensionBean()
throws Exception {
this.contextRunner.withPropertyValues("endpoints.sessions.enabled:false").run(
(context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class));
}

@Configuration
static class SessionConfiguration {

@Bean
public FindByIndexNameSessionRepository sessionRepository() {
return mock(FindByIndexNameSessionRepository.class);
}

}

}
5 changes: 5 additions & 0 deletions spring-boot-actuator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<optional>true</optional>
</dependency>
<!-- Annotation processing -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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.springframework.boot.actuate.session;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;

/**
* {@link Endpoint} to expose a user's {@link Session}s.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@Endpoint(id = "sessions")
public class SessionsEndpoint {

private final FindByIndexNameSessionRepository<? extends Session> sessionRepository;

/**
* Create a new {@link SessionsEndpoint} instance.
* @param sessionRepository the session repository
*/
public SessionsEndpoint(
FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
this.sessionRepository = sessionRepository;
}

@ReadOperation
public SessionsReport sessionsForUsername(String username) {
Map<String, ? extends Session> sessions = this.sessionRepository
.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
username);
return new SessionsReport(sessions);
}

@ReadOperation
public SessionDescriptor getSession(@Selector String sessionId) {
Session session = this.sessionRepository.findById(sessionId);
return new SessionDescriptor(session);
}

@DeleteOperation
public void deleteSession(@Selector String sessionId) {
this.sessionRepository.deleteById(sessionId);
}

/**
* A report of user's {@link Session sessions}. Primarily intended for serialization
* to JSON.
*/
public static final class SessionsReport {

private final List<SessionDescriptor> sessions;

public SessionsReport(Map<String, ? extends Session> sessions) {
this.sessions = sessions.entrySet().stream()
.map(s -> new SessionDescriptor(s.getValue()))
.collect(Collectors.toList());
}

public List<SessionDescriptor> getSessions() {
return this.sessions;
}

}

/**
* A description of user's {@link Session session}. Primarily intended for
* serialization to JSON.
*/
public static final class SessionDescriptor {

private final String id;

private final Set<String> attributeNames;

private final long creationTime;

private final long lastAccessedTime;

private final long maxInactiveInterval;

private final boolean expired;

public SessionDescriptor(Session session) {
this.id = session.getId();
this.attributeNames = session.getAttributeNames();
this.creationTime = session.getCreationTime().toEpochMilli();
this.lastAccessedTime = session.getLastAccessedTime().toEpochMilli();
this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds();
this.expired = session.isExpired();
}

public String getId() {
return this.id;
}

public Set<String> getAttributeNames() {
return this.attributeNames;
}

public long getCreationTime() {
return this.creationTime;
}

public long getLastAccessedTime() {
return this.lastAccessedTime;
}

public long getMaxInactiveInterval() {
return this.maxInactiveInterval;
}

public boolean isExpired() {
return this.expired;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* 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.springframework.boot.actuate.session;

import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointExtension;
import org.springframework.boot.actuate.session.SessionsEndpoint.SessionsReport;

/**
* {@link WebEndpointExtension} for the {@link SessionsEndpoint}.
*
* @author Vedran Pavic
* @since 2.0.0
*/
@WebEndpointExtension(endpoint = SessionsEndpoint.class)
public class SessionsWebEndpointExtension {

private final SessionsEndpoint delegate;

public SessionsWebEndpointExtension(SessionsEndpoint delegate) {
this.delegate = delegate;
}

@ReadOperation
public WebEndpointResponse<SessionsReport> sessionsForUsername(String username) {
if (username == null) {
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
}
SessionsReport sessions = this.delegate.sessionsForUsername(username);
return new WebEndpointResponse<>(sessions);
}

}
Loading