Skip to content

Commit

Permalink
Initial degraded operating mode filter
Browse files Browse the repository at this point in the history
  • Loading branch information
fhanik committed Sep 18, 2017
1 parent 281757b commit 365c103
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 68 deletions.

This file was deleted.

@@ -0,0 +1,134 @@
/*
* ****************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2017] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
* ****************************************************************************
*/
package org.cloudfoundry.identity.uaa.web;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;

public class DegradedModeUaaFilter extends OncePerRequestFilter {

private static Log logger = LogFactory.getLog(DegradedModeUaaFilter.class);

private Set<String> permittedEndpoints = emptySet();
private Set<String> permittedMethods = emptySet();
private List<AntPathRequestMatcher> endpoints = emptyList();
private boolean enabled = false;


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (isEnabled()) {
if ( isMethodAllowed(request) || isEndpointAllowed(request)) {
filterChain.doFilter(request, response);
} else {
logger.debug(format("Operation Not permitted in degraded mode for URL:%s and method:%s",
request.getRequestURI(),
request.getMethod()
)
);
Map<String, String> json = getErrorData();
if (acceptsJson(request)) {
response.setStatus(SC_SERVICE_UNAVAILABLE);
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(JsonUtils.writeValueAsString(json));
response.getWriter().flush();
response.getWriter().close();
} else {
response.sendError(SC_SERVICE_UNAVAILABLE, json.get("description"));
}
}
} else {
filterChain.doFilter(request, response);
}
}

protected Map<String, String> getErrorData() {
String error = "uaa_unavailable";
String description = "UAA intentionally in degraded mode, operation not permitted. Please try later.";
Map<String, String> json = new HashMap<>();
json.put("error", error);
json.put("error_description", description);
return json;
}

protected boolean acceptsJson(HttpServletRequest request) {
List<MediaType> mediaTypes = MediaType.parseMediaTypes(request.getHeader(HttpHeaders.ACCEPT));
return mediaTypes.stream().anyMatch(m -> m.isCompatibleWith(MediaType.APPLICATION_JSON));
}

protected boolean isMethodAllowed(HttpServletRequest request) {
return getPermittedMethods().contains(request.getMethod().toUpperCase());
}

public boolean isEndpointAllowed(HttpServletRequest request) {
return endpoints.stream().anyMatch(m -> m.matches(request));
}

public void setPermittedEndpoints(Set<String> permittedEndpoints) {
this.permittedEndpoints = permittedEndpoints;
if (permittedEndpoints==null) {
this.endpoints = emptyList();
} else {
this.endpoints =
permittedEndpoints
.stream()
.map(s -> new AntPathRequestMatcher(s))
.collect(toList());
}
}


public Set<String> getPermittedEndpoints() {
return permittedEndpoints;
}

public Set<String> getPermittedMethods() {
return permittedMethods;
}

public void setPermittedMethods(Set<String> permittedMethods) {
this.permittedMethods = ofNullable(permittedMethods).orElse(emptySet());
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public boolean isEnabled() {
return enabled;
}
}
@@ -0,0 +1,147 @@
/*
* ****************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2017] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
* ****************************************************************************
*/

package org.cloudfoundry.identity.uaa.web;

import org.cloudfoundry.identity.uaa.util.JsonUtils;
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.util.Arrays;
import java.util.HashSet;

import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.springframework.http.HttpHeaders.ACCEPT;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;

public class DegradedModeUaaFilterTests {

private MockHttpServletRequest request;
private MockHttpServletResponse response;
private FilterChain chain;
private DegradedModeUaaFilter filter;

@Before
public void setup() throws Exception {
request = new MockHttpServletRequest();
request.addHeader(ACCEPT, "*/*");
response = new MockHttpServletResponse();
chain = mock(FilterChain.class);
filter = new DegradedModeUaaFilter();
}

public void setPathInfo(String pathInfo) {
request.setServletPath("");
request.setPathInfo(pathInfo);
request.setContextPath("/uaa");
request.setRequestURI(request.getContextPath()+request.getPathInfo());
}

@Test
public void disabled() throws Exception {
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(same(request), same(response));
}

@Test
public void enabled_no_whitelist_post() throws Exception {
request.setMethod(POST.name());
filter.setEnabled(true);
filter.doFilterInternal(request, response, chain);
verifyZeroInteractions(chain);
assertEquals(SC_SERVICE_UNAVAILABLE, response.getStatus());
}

@Test
public void enabled_no_whitelist_get() throws Exception {
request.setMethod(GET.name());
filter.setEnabled(true);
filter.setPermittedMethods(new HashSet<>(Arrays.asList(GET.toString())));
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(same(request), same(response));
}

@Test
public void enabled_matching_url_post() throws Exception {
request.setMethod(POST.name());
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
for (String pathInfo : Arrays.asList("/oauth/token", "/oauth/token/alias/something")) {
setPathInfo(pathInfo);
reset(chain);
filter.doFilterInternal(request, response, chain);
verify(chain, times(1)).doFilter(same(request), same(response));
}
}

@Test
public void enabled_not_matching_post() throws Exception {
request.setMethod(POST.name());
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
for (String pathInfo : Arrays.asList("/url", "/other/url")) {
response = new MockHttpServletResponse();
setPathInfo(pathInfo);
reset(chain);
filter.doFilterInternal(request, response, chain);
verifyZeroInteractions(chain);
assertEquals(SC_SERVICE_UNAVAILABLE, response.getStatus());
}
}

@Test
public void error_is_json() throws Exception {
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
for (String accept : Arrays.asList("application/json", "text/html,*/*")) {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
setPathInfo("/not/allowed");
request.setMethod(POST.name());
request.addHeader(ACCEPT, accept);
filter.doFilterInternal(request, response, chain);
assertEquals(SC_SERVICE_UNAVAILABLE, response.getStatus());
assertEquals(JsonUtils.writeValueAsString(filter.getErrorData()), response.getContentAsString());
}
}

@Test
public void error_is_not() throws Exception {
filter.setPermittedEndpoints(new HashSet(Arrays.asList("/oauth/token/**")));
filter.setEnabled(true);
for (String accept : Arrays.asList("text/html", "text/plain")) {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
setPathInfo("/not/allowed");
request.setMethod(POST.name());
request.addHeader(ACCEPT, accept);
filter.doFilterInternal(request, response, chain);
assertEquals(SC_SERVICE_UNAVAILABLE, response.getStatus());
assertEquals(filter.getErrorData().get("description"), response.getErrorMessage());
}
}

}
14 changes: 14 additions & 0 deletions uaa/src/main/resources/uaa.yml
Expand Up @@ -589,6 +589,20 @@ uaa:
url: http://localhost:8080/uaa/approvals
login:
url: http://localhost:8080/uaa/authenticate
degraded:
enabled: false
whitelist:
endpoints:
- /oauth/authorize/**
- /oauth/token/**
- /check_token
- /login/**
- /logout/**
- /saml/**
methods:
- GET
- HEAD
- OPTIONS

# Google Analytics
#analytics:
Expand Down
28 changes: 13 additions & 15 deletions uaa/src/main/webapp/WEB-INF/spring-servlet.xml
Expand Up @@ -550,20 +550,18 @@
<property name="userDatabase" ref="userDatabase"/>
</bean>

<beans profile="degraded">
<util:set id="degradedModeEndpoints">
<value>/oauth/authorize</value>
<value>/oauth/token</value>
<value>/check_token</value>
<value>/login.do</value>
<value>/login/idp_discovery</value>
</util:set>
<bean id="degradedModeUaaFilter" class="org.cloudfoundry.identity.uaa.mode.degraded.DegradedModeUaaFilter">
<property name="permittedEndpoints" ref="degradedModeEndpoints"/>
</bean>
</beans>
<beans profile="!degraded">
<bean id="degradedModeUaaFilter" class="org.cloudfoundry.identity.uaa.web.NoOpFilter" />
</beans>
<bean id="degradedModeUaaFilter" class="org.cloudfoundry.identity.uaa.web.DegradedModeUaaFilter">
<property name="enabled" value="${uaa.degraded.enabled:false}"/>
<property name="permittedEndpoints"
value="#{@config['uaa']==null ? null :
@config['uaa']['degraded']==null ? null :
@config['uaa']['degraded']['whitelist']==null ? null :
@config['uaa']['degraded']['whitelist']['endpoints']}"/>
<property name="permittedMethods"
value="#{@config['uaa']==null ? null :
@config['uaa']['degraded']==null ? null :
@config['uaa']['degraded']['whitelist']==null ? null :
@config['uaa']['degraded']['whitelist']['methods']}"/>
</bean>

</beans>

0 comments on commit 365c103

Please sign in to comment.