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

Allow for OPTIONS requests to be passed through auth filters #1244

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public void run(ReaperApplicationConfiguration config, Environment environment)
if (config.isEnableCrossOrigin() || System.getProperty("enableCrossOrigin") != null) {
FilterRegistration.Dynamic co = environment.servlets().addFilter("crossOriginRequests", CrossOriginFilter.class);
co.setInitParameter("allowedOrigins", "*");
co.setInitParameter("allowedHeaders", "X-Requested-With,Content-Type,Accept,Origin");
co.setInitParameter("allowedHeaders", "X-Requested-With,Content-Type,Accept,Origin,Authorization");
co.setInitParameter("allowedMethods", "OPTIONS,GET,PUT,POST,DELETE,HEAD,PATCH");
co.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
}
Expand All @@ -205,7 +205,6 @@ public void run(ReaperApplicationConfiguration config, Environment environment)
environment.jersey().register(pingResource);

final ClusterResource addClusterResource = ClusterResource.create(context, cryptograph);

environment.jersey().register(addClusterResource);
final RepairRunResource addRepairRunResource = new RepairRunResource(context);
environment.jersey().register(addRepairRunResource);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
*
* Copyright 2022-2022 The Last Pickle Ltd
*
* 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 io.cassandrareaper.resources;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;

import com.google.common.annotations.VisibleForTesting;

public final class RequestUtils {
public static final String ALLOW_ALL_OPTIONS_REQUESTS_ENV_VAR_NAME = "ALLOW_ALL_OPTIONS_REQUESTS";

private RequestUtils() {}

public static boolean isOptionsRequest(ServletRequest request) {
if (request != null && request instanceof HttpServletRequest) {
if (((HttpServletRequest) request).getMethod().equalsIgnoreCase(HttpMethod.OPTIONS)) {
return true;
}
}
return false;
}

@VisibleForTesting
static boolean isAllowAllOptionsRequests(String allowAllOptionsRequestsEnvVarValue) {
if (allowAllOptionsRequestsEnvVarValue != null) {
return Boolean.parseBoolean(allowAllOptionsRequestsEnvVarValue.trim().toLowerCase());
}
return false;
}

public static boolean isAllowAllOptionsRequests() {
String allowAllOptionsRequestsEnvVarValue = System.getenv(ALLOW_ALL_OPTIONS_REQUESTS_ENV_VAR_NAME);
return isAllowAllOptionsRequests(allowAllOptionsRequestsEnvVarValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,39 @@

package io.cassandrareaper.resources.auth;

import io.cassandrareaper.resources.RequestUtils;

import java.io.IOException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;
import org.apache.shiro.web.util.WebUtils;

public final class RestPermissionsFilter extends HttpMethodPermissionFilter {
private final boolean allowAllOptionsRequests;

public RestPermissionsFilter() {
allowAllOptionsRequests = RequestUtils.isAllowAllOptionsRequests();
}

public RestPermissionsFilter() {}
@VisibleForTesting
boolean isAllowAllOptionsRequests() {
return allowAllOptionsRequests;
}

@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws IOException {
if (isAllowAllOptionsRequests() && RequestUtils.isOptionsRequest(request)) {
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}

@Override
protected Subject getSubject(ServletRequest request, ServletResponse response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,51 @@

package io.cassandrareaper.resources.auth;

import io.cassandrareaper.resources.RequestUtils;

import java.util.Optional;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import com.google.common.annotations.VisibleForTesting;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.lang.Strings;

import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.subject.WebSubject;
import org.apache.shiro.web.util.WebUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class ShiroJwtVerifyingFilter extends AccessControlFilter {

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

public ShiroJwtVerifyingFilter() {}
private final boolean allowAllOptionsRequests;

public ShiroJwtVerifyingFilter() {
allowAllOptionsRequests = RequestUtils.isAllowAllOptionsRequests();
}

@VisibleForTesting
boolean isAllowAllOptionsRequests() {
return allowAllOptionsRequests;
}

@Override
protected boolean isAccessAllowed(ServletRequest req, ServletResponse res, Object mappedValue) throws Exception {
if (isAllowAllOptionsRequests() && RequestUtils.isOptionsRequest(req)) {
return true;
}

Subject nonJwt = getSubject(req, res);

return null != nonJwt.getPrincipal() && (nonJwt.isRemembered() || nonJwt.isAuthenticated())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
*
* Copyright 2019-2019 The Last Pickle Ltd
*
* 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 io.cassandrareaper.resources;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;

import org.assertj.core.api.Assertions;
import org.junit.Test;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

public class RequestUtilsTest {
@Test
public void testGetAllowAllOptionsRequestsFromEnvironmentWithEmptyEnvironmentReturnsFalse() {
boolean allowAll = RequestUtils.isAllowAllOptionsRequests();
Assertions.assertThat(allowAll).isFalse();
}

@Test
public void testGetAllowAllOptionsRequestsFromEnvironmentWithTrueEnvironmentReturnsTrue() {
boolean allowAll = RequestUtils.isAllowAllOptionsRequests("true");
Assertions.assertThat(allowAll).isTrue();
}

@Test
public void testGetAllowAllOptionsRequestsFromEnvironmentWithFalseEnvironmentReturnsFalse() {
boolean allowAll = RequestUtils.isAllowAllOptionsRequests("false");
Assertions.assertThat(allowAll).isFalse();
}

@Test
public void testGetAllowAllOptionsRequestsFromInvalidEnvironmentWithEnvironmentReturnsFalse() {
boolean allowAll = RequestUtils.isAllowAllOptionsRequests("bad");
Assertions.assertThat(allowAll).isFalse();
}

@Test
public void testIsOptionsRequestInvalidInputReturnsFalse() {
boolean isOptionsRequest = RequestUtils.isOptionsRequest(null);
Assertions.assertThat(isOptionsRequest).isFalse();
}

@Test
public void testIsOptionsRequestOptionsServletInputReturnsTrue() {
HttpServletRequest mockServletRequest = spy(HttpServletRequest.class);
when(mockServletRequest.getMethod()).thenReturn(HttpMethod.OPTIONS);
boolean isOptionsRequest = RequestUtils.isOptionsRequest(mockServletRequest);
Assertions.assertThat(isOptionsRequest).isTrue();
}

@Test
public void testIsOptionsRequestGetServletInputReturnsTrue() {
HttpServletRequest mockServletRequest = spy(HttpServletRequest.class);
when(mockServletRequest.getMethod()).thenReturn(HttpMethod.GET);
boolean isOptionsRequest = RequestUtils.isOptionsRequest(mockServletRequest);
Assertions.assertThat(isOptionsRequest).isFalse();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
*
* Copyright 2022-2022 The Last Pickle Ltd
*
* 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 io.cassandrareaper.resources.auth;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.mockito.Mockito;

public class RestPermissionsFilterTest {

@Test
public void testOptionsRequestWithoutAuthorizationIsAllowed() throws Exception {
RestPermissionsFilter filter = Mockito.spy(RestPermissionsFilter.class);
HttpServletRequest mockHttpServletRequest = Mockito.spy(HttpServletRequest.class);
Mockito.when(mockHttpServletRequest.getMethod()).thenReturn(HttpMethod.OPTIONS);
Mockito.when(filter.isAllowAllOptionsRequests()).thenReturn(true);

boolean allowed = filter.isAccessAllowed(
mockHttpServletRequest,
Mockito.mock(ServletResponse.class),
Mockito.mock(Object.class)
);
Assertions.assertThat(allowed).isTrue();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.security.Principal;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HttpMethod;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
Expand All @@ -31,7 +32,6 @@
import org.junit.Test;
import org.mockito.Mockito;


public final class ShiroJwtVerifyingFilterTest {

@Test
Expand Down Expand Up @@ -199,5 +199,19 @@ public void testAuthorizationValid() throws Exception {
}
}

@Test
public void testOptionsRequestWithoutAuthorizationIsAllowed() throws Exception {
ShiroJwtVerifyingFilter filter = Mockito.spy(ShiroJwtVerifyingFilter.class);
HttpServletRequest mockHttpServletRequest = Mockito.spy(HttpServletRequest.class);
Mockito.when(mockHttpServletRequest.getMethod()).thenReturn(HttpMethod.OPTIONS);
Mockito.when(filter.isAllowAllOptionsRequests()).thenReturn(true);

boolean allowed = filter.isAccessAllowed(
mockHttpServletRequest,
Mockito.mock(ServletResponse.class),
Mockito.mock(Object.class)
);
Assertions.assertThat(allowed).isTrue();
}

}