Skip to content
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

ReadOnly mode for Management #1025

Merged
merged 3 commits into from
May 30, 2019
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pl.allegro.tech.hermes.api.endpoints;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;

@Path("mode")
public interface ModeEndpoint {

@GET
@Produces(APPLICATION_JSON)
String getMode();

@POST
@Produces(APPLICATION_JSON)
Response setMode(@QueryParam("mode") String mode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package pl.allegro.tech.hermes.management.api;

import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Component;
import pl.allegro.tech.hermes.management.api.auth.Roles;
import pl.allegro.tech.hermes.management.domain.mode.ModeService;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static pl.allegro.tech.hermes.management.domain.mode.ModeService.ManagementMode.READ_ONLY;
import static pl.allegro.tech.hermes.management.domain.mode.ModeService.ManagementMode.READ_WRITE;

@Component
@Path("/mode")
@Api(value = "/mode", description = "Operations on management mode")
public class ModeEndpoint {

private final ModeService modeService;

public ModeEndpoint(ModeService modeService) {
this.modeService = modeService;
}

@GET
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Get management mode", response = String.class, httpMethod = HttpMethod.GET)
public String getMode() {
return modeService.getMode().toString();
cristaloleg marked this conversation as resolved.
Show resolved Hide resolved
}

@POST
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Set management mode", response = String.class, httpMethod = HttpMethod.POST)
@RolesAllowed(Roles.ADMIN)
public Response setMode(@QueryParam("mode") String mode) {
if (mode == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
switch (mode) {
case ModeService.READ_WRITE:
modeService.setMode(READ_WRITE);
break;
case ModeService.READ_ONLY:
modeService.setMode(READ_ONLY);
break;
default:
return Response.status(Response.Status.BAD_REQUEST).build();
}
return Response.status(Response.Status.OK).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pl.allegro.tech.hermes.management.api;

import static javax.servlet.http.HttpServletResponse.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.GenericFilterBean;
import pl.allegro.tech.hermes.management.domain.mode.ModeService;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

@WebFilter(urlPatterns = "/*")
public class ReadOnlyFilter extends GenericFilterBean {

private static final Logger logger = LoggerFactory.getLogger(ReadOnlyFilter.class);
private static final String READ_ONLY_ERROR_MESSAGE = "Action forbidden due to read-only mode";

private final ModeService modeService;

public ReadOnlyFilter(ModeService modeService) {
this.modeService = modeService;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (modeService.isReadOnlyEnabled()) {
HttpServletRequest req = (HttpServletRequest) request;
if (!req.getMethod().equals("GET") && !isWhitelisted(req.getRequestURI())) {
HttpServletResponse resp = ((HttpServletResponse) response);
resp.sendError(SC_SERVICE_UNAVAILABLE, READ_ONLY_ERROR_MESSAGE);
cristaloleg marked this conversation as resolved.
Show resolved Hide resolved
return;
}
}
chain.doFilter(request, response);
}

private boolean isWhitelisted(String requestURI) {
if (requestURI.startsWith("/query")) {
return true;
}
if (requestURI.startsWith("/mode")) {
return true;
}
if (requestURI.startsWith("/topics") && requestURI.endsWith("query")) {
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import static javax.servlet.DispatcherType.REQUEST;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import pl.allegro.tech.hermes.common.clock.ClockFactory;
import pl.allegro.tech.hermes.management.api.ReadOnlyFilter;
import pl.allegro.tech.hermes.management.domain.mode.ModeService;
import pl.allegro.tech.hermes.management.domain.subscription.SubscriptionLagSource;
import pl.allegro.tech.hermes.management.infrastructure.metrics.NoOpSubscriptionLagSource;

import javax.servlet.DispatcherType;
import java.time.Clock;
import java.util.EnumSet;

@Configuration
@EnableConfigurationProperties({TopicProperties.class, MetricsProperties.class, HttpClientProperties.class})
Expand Down Expand Up @@ -46,4 +52,13 @@ public SubscriptionLagSource consumerLagSource() {
public Clock clock() {
return new ClockFactory().provide();
}

@Bean
public FilterRegistrationBean<ReadOnlyFilter> readOnlyFilter(ModeService modeService) {
FilterRegistrationBean<ReadOnlyFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setDispatcherTypes(REQUEST);
registrationBean.setFilter(new ReadOnlyFilter(modeService));
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package pl.allegro.tech.hermes.management.domain.mode;

import org.springframework.stereotype.Component;

@Component
public class ModeService {

public static final String READ_WRITE = "readWrite";
public static final String READ_ONLY = "readOnly";

public enum ManagementMode {
READ_WRITE(ModeService.READ_WRITE),
READ_ONLY(ModeService.READ_ONLY);

private final String text;

ManagementMode(String text) {
this.text = text;
}

@Override
public String toString() {
return text;
}
}

private volatile ManagementMode mode = ManagementMode.READ_WRITE;
cristaloleg marked this conversation as resolved.
Show resolved Hide resolved

public ManagementMode getMode() {
return mode;
}

public void setMode(ManagementMode mode) {
this.mode = mode;
}

public boolean isReadOnlyEnabled() {
cristaloleg marked this conversation as resolved.
Show resolved Hide resolved
return mode == ManagementMode.READ_ONLY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pl.allegro.tech.hermes.api.endpoints.BlacklistEndpoint;
import pl.allegro.tech.hermes.api.endpoints.GroupEndpoint;
import pl.allegro.tech.hermes.api.endpoints.MigrationEndpoint;
import pl.allegro.tech.hermes.api.endpoints.ModeEndpoint;
import pl.allegro.tech.hermes.api.endpoints.OAuthProviderEndpoint;
import pl.allegro.tech.hermes.api.endpoints.SubscriptionOwnershipEndpoint;
import pl.allegro.tech.hermes.api.endpoints.OwnerEndpoint;
Expand Down Expand Up @@ -114,6 +115,10 @@ public UnhealthyEndpoint unhealthyEndpoint() {
return createProxy(url, UnhealthyEndpoint.class, managementConfig);
}

public ModeEndpoint modeEndpoint() {
return createProxy(url, ModeEndpoint.class, managementConfig);
}

public BlacklistEndpoint createBlacklistEndpoint() {
return createProxy(url, BlacklistEndpoint.class, managementConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pl.allegro.tech.hermes.api.endpoints.BlacklistEndpoint;
import pl.allegro.tech.hermes.api.endpoints.GroupEndpoint;
import pl.allegro.tech.hermes.api.endpoints.MigrationEndpoint;
import pl.allegro.tech.hermes.api.endpoints.ModeEndpoint;
import pl.allegro.tech.hermes.api.endpoints.OAuthProviderEndpoint;
import pl.allegro.tech.hermes.api.endpoints.OwnerEndpoint;
import pl.allegro.tech.hermes.api.endpoints.QueryEndpoint;
Expand Down Expand Up @@ -41,7 +42,9 @@ public class HermesEndpoints {

private final MigrationEndpoint migrationEndpoint;

public final UnhealthyEndpoint unhealthyEndpoint;
private final UnhealthyEndpoint unhealthyEndpoint;

private final ModeEndpoint modeEndpoint;

public HermesEndpoints(Hermes hermes) {
this.groupEndpoint = hermes.createGroupEndpoint();
Expand All @@ -56,6 +59,7 @@ public HermesEndpoints(Hermes hermes) {
this.ownerEndpoint = hermes.createOwnerEndpoint();
this.migrationEndpoint = hermes.createMigrationEndpoint();
this.unhealthyEndpoint = hermes.unhealthyEndpoint();
this.modeEndpoint = hermes.modeEndpoint();
}

public HermesEndpoints(String hermesFrontendUrl, String consumerUrl) {
Expand Down Expand Up @@ -108,6 +112,10 @@ public BlacklistEndpoint blacklist() {
return blacklistEndpoint;
}

public ModeEndpoint modeEndpoint() {
return modeEndpoint;
}

public List<String> findTopics(Topic topic, boolean tracking) {
return topicEndpoint.list(topic.getName().getGroupName(), tracking);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package pl.allegro.tech.hermes.integration.management;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import pl.allegro.tech.hermes.api.Group;
import pl.allegro.tech.hermes.integration.IntegrationTest;
import static pl.allegro.tech.hermes.integration.test.HermesAssertions.assertThat;
import pl.allegro.tech.hermes.management.domain.mode.ModeService;
import static pl.allegro.tech.hermes.test.helper.builder.GroupBuilder.group;

import javax.ws.rs.core.Response;

public class ReadOnlyModeTest extends IntegrationTest {

@BeforeMethod
public void initialize() {
management.modeEndpoint().setMode(ModeService.READ_WRITE);
}

@Test
public void shouldAllowNonModifyingOperations() {
// given
management.modeEndpoint().setMode(ModeService.READ_WRITE);
String groupName = "allowed-group";

// when
Response response = createGroup(groupName);

// then
assertThat(response).hasStatus(Response.Status.CREATED);
}

@Test
public void shouldRestrictModifyingOperations() {
// given
management.modeEndpoint().setMode(ModeService.READ_ONLY);
String groupName = "not-allowed-group";

// when
Response response = createGroup(groupName);

// then
assertThat(response).hasStatus(Response.Status.SERVICE_UNAVAILABLE);
}

@Test
public void shouldSwitchModeBack() {
// given
management.modeEndpoint().setMode(ModeService.READ_ONLY);
String groupName = "not-allowed-at-first-group";

// when
Response response = createGroup(groupName);

// then
assertThat(response).hasStatus(Response.Status.SERVICE_UNAVAILABLE);

// and
management.modeEndpoint().setMode(ModeService.READ_WRITE);

// when
response = createGroup(groupName);

// then
assertThat(response).hasStatus(Response.Status.CREATED);
}

public Response createGroup(String groupName) {
Group group = group(groupName).build();
return management.group().create(group);
}
}