Skip to content

Commit

Permalink
Add the ability to use the file system to toggle limited mode on/off
Browse files Browse the repository at this point in the history
  • Loading branch information
fhanik committed Oct 23, 2017
1 parent 6045699 commit aaf2de4
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 48 deletions.
Expand Up @@ -17,6 +17,8 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.util.TimeService;
import org.cloudfoundry.identity.uaa.util.TimeServiceImpl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
Expand All @@ -26,11 +28,13 @@
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.String.format;
import static java.util.Collections.emptyList;
Expand All @@ -43,12 +47,16 @@ public class LimitedModeUaaFilter extends OncePerRequestFilter {

public static final String ERROR_CODE = "uaa_unavailable";
public static final String ERROR_MESSAGE = "UAA intentionally in limited mode, operation not permitted. Please try later.";
public static final long STATUS_INTERVAL_MS = 5000;
private static Log logger = LogFactory.getLog(LimitedModeUaaFilter.class);

private Set<String> permittedEndpoints = emptySet();
private Set<String> permittedMethods = emptySet();
private List<AntPathRequestMatcher> endpoints = emptyList();
private boolean enabled = false;
private volatile boolean enabled = false;
private File statusFile = null;
private TimeService timeService = new TimeServiceImpl();
private AtomicLong lastFileCheck= new AtomicLong(0);


@Override
Expand Down Expand Up @@ -126,11 +134,38 @@ public void setPermittedMethods(Set<String> permittedMethods) {
this.permittedMethods = ofNullable(permittedMethods).orElse(emptySet());
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
public boolean isTimeToCheckFileSystem() {
long time = lastFileCheck.get();
long now = timeService.getCurrentTimeMillis();
if (now - time > STATUS_INTERVAL_MS && lastFileCheck.compareAndSet(time, now)) {
return true;
}
return false;
}

public boolean isEnabled() {
if (statusFile == null) {
enabled = false;
} else if (isTimeToCheckFileSystem()){
enabled = statusFile.exists();
}
return enabled;
}

public File getStatusFile() {
return statusFile;
}

public void setStatusFile(File statusFile) {
this.statusFile = statusFile;
lastFileCheck.set(0);
}

public void setTimeService(TimeService ts) {
this.timeService = ts;
}

public long getLastFileSystemCheck() {
return lastFileCheck.get();
}
}
Expand Up @@ -16,20 +16,29 @@
package org.cloudfoundry.identity.uaa.web;

import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.util.TimeService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import javax.servlet.FilterChain;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicLong;

import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import static org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter.STATUS_INTERVAL_MS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
Expand All @@ -43,14 +52,29 @@ public class LimitedModeUaaFilterTests {
private MockHttpServletResponse response;
private FilterChain chain;
private LimitedModeUaaFilter filter;
private File statusFile;
private final AtomicLong time = new AtomicLong(System.currentTimeMillis());
TimeService timeService;

@Before
public void setup() throws Exception {
timeService = new TimeService() {
@Override
public long getCurrentTimeMillis() {
return time.get();
}
};
request = new MockHttpServletRequest();
request.addHeader(ACCEPT, "*/*");
response = new MockHttpServletResponse();
chain = mock(FilterChain.class);
filter = new LimitedModeUaaFilter();
statusFile = File.createTempFile("uaa-limited-mode.", ".status");
}

@After
public void teardown() throws Exception {
statusFile.delete();
}

public void setPathInfo(String pathInfo) {
Expand All @@ -64,12 +88,13 @@ public void setPathInfo(String pathInfo) {
public void disabled() throws Exception {
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(same(request), same(response));
assertFalse(filter.isEnabled());
}

@Test
public void enabled_no_whitelist_post() throws Exception {
request.setMethod(POST.name());
filter.setEnabled(true);
filter.setStatusFile(statusFile);
filter.doFilterInternal(request, response, chain);
verifyZeroInteractions(chain);
assertEquals(SC_SERVICE_UNAVAILABLE, response.getStatus());
Expand All @@ -78,7 +103,7 @@ public void enabled_no_whitelist_post() throws Exception {
@Test
public void enabled_no_whitelist_get() throws Exception {
request.setMethod(GET.name());
filter.setEnabled(true);
filter.setStatusFile(statusFile);
filter.setPermittedMethods(new HashSet<>(Arrays.asList(GET.toString())));
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(same(request), same(response));
Expand All @@ -88,7 +113,7 @@ public void enabled_no_whitelist_get() throws Exception {
public void enabled_matching_url_post() throws Exception {
request.setMethod(POST.name());
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
filter.setStatusFile(statusFile);
for (String pathInfo : Arrays.asList("/oauth/token", "/oauth/token/alias/something")) {
setPathInfo(pathInfo);
reset(chain);
Expand All @@ -101,7 +126,7 @@ public void enabled_matching_url_post() throws Exception {
public void enabled_not_matching_post() throws Exception {
request.setMethod(POST.name());
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
filter.setStatusFile(statusFile);
for (String pathInfo : Arrays.asList("/url", "/other/url")) {
response = new MockHttpServletResponse();
setPathInfo(pathInfo);
Expand All @@ -115,7 +140,7 @@ public void enabled_not_matching_post() throws Exception {
@Test
public void error_is_json() throws Exception {
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
filter.setStatusFile(statusFile);
for (String accept : Arrays.asList("application/json", "text/html,*/*")) {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
Expand All @@ -131,7 +156,7 @@ public void error_is_json() throws Exception {
@Test
public void error_is_not() throws Exception {
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
filter.setStatusFile(statusFile);
for (String accept : Arrays.asList("text/html", "text/plain")) {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
Expand All @@ -144,4 +169,26 @@ public void error_is_not() throws Exception {
}
}

@Test
public void disable_enable_uses_cache_to_avoid_file_access() throws Exception {
File spy = spy(statusFile);
doCallRealMethod().when(spy).exists();
filter.setTimeService(timeService);
filter.setStatusFile(spy);
assertTrue(filter.isEnabled());
statusFile.delete();
for (int i=0; i<10; i++) assertTrue(filter.isEnabled());
time.set(time.get() + STATUS_INTERVAL_MS + 10);
assertFalse(filter.isEnabled());
verify(spy, times(2)).exists();
}

@Test
public void settings_file_changes_cache() throws Exception {
disable_enable_uses_cache_to_avoid_file_access();
filter.setStatusFile(null);
assertFalse(filter.isEnabled());
assertEquals(0, filter.getLastFileSystemCheck());
}

}
2 changes: 1 addition & 1 deletion uaa/src/main/webapp/WEB-INF/spring-servlet.xml
Expand Up @@ -569,7 +569,7 @@
</bean>

<bean id="limitedModeUaaFilter" class="org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter">
<property name="enabled" value="${uaa.limitedFunctionality.enabled:false}"/>
<property name="statusFile" value="${uaa.limitedFunctionality.statusFile:#{null}}"/>
<property name="permittedEndpoints"
value="#{@config['uaa']==null ? null :
@config['uaa']['limitedFunctionality']==null ? null :
Expand Down
Expand Up @@ -218,6 +218,7 @@ public void defaults_and_required_properties() throws Exception {
assertTrue(metricsFilter.isEnabled());

LimitedModeUaaFilter limitedModeUaaFilter = context.getBean(LimitedModeUaaFilter.class);
assertNull(limitedModeUaaFilter.getStatusFile());
assertFalse(limitedModeUaaFilter.isEnabled());
assertThat(limitedModeUaaFilter.getPermittedEndpoints(),
containsInAnyOrder(
Expand Down Expand Up @@ -477,7 +478,7 @@ public void all_properties_set() throws Exception {
assertFalse(metricsFilter.isEnabled());

LimitedModeUaaFilter limitedModeUaaFilter = context.getBean(LimitedModeUaaFilter.class);
assertTrue(limitedModeUaaFilter.isEnabled());
assertEquals("/tmp/uaa-test-limited-mode-status-file.txt", limitedModeUaaFilter.getStatusFile().getAbsolutePath());
assertThat(limitedModeUaaFilter.getPermittedEndpoints(),
containsInAnyOrder(
"/oauth/authorize/**",
Expand Down
Expand Up @@ -16,22 +16,30 @@
package org.cloudfoundry.identity.uaa.mock.limited;

import org.cloudfoundry.identity.uaa.mock.token.JwtBearerGrantMockMvcTests;
import org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter;
import org.junit.After;
import org.junit.Before;

import java.io.File;

import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getLimitedModeStatusFile;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.resetLimitedModeStatusFile;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.setLimitedModeStatusFile;

public class LimitedModeJwtBearerGrantMockMvcTests extends JwtBearerGrantMockMvcTests {
private boolean original;
private File existingStatusFile;
private File statusFile;

@Before
public void setup () throws Exception {
LimitedModeUaaFilter bean = getWebApplicationContext().getBean(LimitedModeUaaFilter.class);
original = bean.isEnabled();
bean.setEnabled(true);
@Override
public void setUpContext() throws Exception {
super.setUpContext();
existingStatusFile = getLimitedModeStatusFile(getWebApplicationContext());
statusFile = setLimitedModeStatusFile(getWebApplicationContext());
}


@After
public void teardown() throws Exception {
getWebApplicationContext().getBean(LimitedModeUaaFilter.class).setEnabled(original);
public void tearDown() throws Exception {
resetLimitedModeStatusFile(getWebApplicationContext(), existingStatusFile);
}
}
Expand Up @@ -16,29 +16,34 @@
package org.cloudfoundry.identity.uaa.mock.limited;

import org.cloudfoundry.identity.uaa.login.LoginMockMvcTests;
import org.cloudfoundry.identity.uaa.web.LimitedModeUaaFilter;
import org.junit.After;
import org.junit.Before;

import java.io.File;

import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getLimitedModeStatusFile;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.resetLimitedModeStatusFile;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.setLimitedModeStatusFile;

public class LimitedModeLoginMockMvcTests extends LoginMockMvcTests {

private boolean original;
private File statusFile;
private File existingStatusFile = null;

@Before
@Override
public void setUpContext() throws Exception {
super.setUpContext();
LimitedModeUaaFilter bean = getWebApplicationContext().getBean(LimitedModeUaaFilter.class);
original = bean.isEnabled();
bean.setEnabled(true);
existingStatusFile = getLimitedModeStatusFile(getWebApplicationContext());
statusFile = setLimitedModeStatusFile(getWebApplicationContext());
}


@After
@Override
public void tearDown() throws Exception {
super.tearDown();
getWebApplicationContext().getBean(LimitedModeUaaFilter.class).setEnabled(original);
resetLimitedModeStatusFile(getWebApplicationContext(), existingStatusFile);
}

}
Expand Up @@ -26,6 +26,11 @@
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.io.File;

import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.getLimitedModeStatusFile;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.resetLimitedModeStatusFile;
import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.setLimitedModeStatusFile;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE;
Expand All @@ -37,14 +42,15 @@

public class LimitedModeNegativeTests extends InjectedMockContextTest {

private boolean limitedMode;
private String adminToken;
private File statusFile;
private File existingStatusFile = null;

@Before
public void setUp() throws Exception {
LimitedModeUaaFilter bean = getWebApplicationContext().getBean(LimitedModeUaaFilter.class);
limitedMode = bean.isEnabled();
bean.setEnabled(true);
existingStatusFile = getLimitedModeStatusFile(getWebApplicationContext());
statusFile = setLimitedModeStatusFile(getWebApplicationContext());

adminToken = MockMvcUtils.getClientCredentialsOAuthAccessToken(getMockMvc(),
"admin",
"adminsecret",
Expand All @@ -56,7 +62,7 @@ public void setUp() throws Exception {

@After
public void tearDown() throws Exception {
getWebApplicationContext().getBean(LimitedModeUaaFilter.class).setEnabled(limitedMode);
resetLimitedModeStatusFile(getWebApplicationContext(), existingStatusFile);
}


Expand Down

0 comments on commit aaf2de4

Please sign in to comment.