Skip to content

Commit

Permalink
Make forbidden resource response code configurable
Browse files Browse the repository at this point in the history
Resolves #454

This makes it possible to "hide" forbidden resources. That is, rather
than responding with a 403 Forbidden code, an admin can configure the
server to respond with a 404 Not Found instead. This may be useful for
applications that wish to have greater privacy controls.
  • Loading branch information
acoburn committed Aug 1, 2019
1 parent b2d115c commit 15cd821
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import javax.inject.Inject;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
Expand Down Expand Up @@ -100,6 +101,13 @@ public class WebAcFilter implements ContainerRequestFilter, ContainerResponseFil
/** The configuration key controlling the realm used in a WWW-Authenticate header, or 'trellis' by default. **/
public static final String CONFIG_WEBAC_REALM = "trellis.webac.realm";

/**
* The configuration key controlling the response code for forbidden resources.
*
* <p>A true value will cause 404 Not Found responses to be generated for forbidden resources.
*/
public static final String CONFIG_WEBAC_HIDE_FORBIDDEN_RESOURCES = "trellis.webac.hide.forbidden.resources";

private static final Logger LOGGER = getLogger(WebAcFilter.class);
private static final RDF rdf = getInstance();
private static final String ORIGIN = "Origin";
Expand All @@ -110,6 +118,7 @@ public class WebAcFilter implements ContainerRequestFilter, ContainerResponseFil
protected final WebAcService accessService;
private final List<String> challenges;
private final String baseUrl;
private final boolean hideForbiddenResources;

/**
* No-op constructor for CDI.
Expand All @@ -118,6 +127,7 @@ public class WebAcFilter implements ContainerRequestFilter, ContainerResponseFil
this.accessService = null;
this.challenges = null;
this.baseUrl = null;
this.hideForbiddenResources = false;
}

/**
Expand All @@ -134,6 +144,7 @@ private WebAcFilter(final WebAcService accessService, final Config config) {
this(accessService,
asList(config.getOptionalValue(CONFIG_WEBAC_CHALLENGES, String.class).orElse("").split(",")),
config.getOptionalValue(CONFIG_WEBAC_REALM, String.class).orElse("trellis"),
config.getOptionalValue(CONFIG_WEBAC_HIDE_FORBIDDEN_RESOURCES, Boolean.class).orElse(false),
config.getOptionalValue(CONFIG_HTTP_BASE_URL, String.class).orElse(null));
}

Expand All @@ -143,12 +154,15 @@ private WebAcFilter(final WebAcService accessService, final Config config) {
* @param accessService the access service
* @param challengeTypes the WWW-Authenticate challenge types
* @param realm the authentication realm
* @param hideForbiddenResources true indicates using a 404 response code for forbidden resources, thereby
* hiding them from clients; otherwise, 403 response code will be generated for forbidden resources
* @param baseUrl the base URL, may be null
*/
public WebAcFilter(final WebAcService accessService, final List<String> challengeTypes,
final String realm, final String baseUrl) {
final String realm, final boolean hideForbiddenResources, final String baseUrl) {
requireNonNull(challengeTypes, "Challenges may not be null!");
requireNonNull(realm, "Realm may not be null!");
this.hideForbiddenResources = hideForbiddenResources;
this.accessService = requireNonNull(accessService, "Access Control service may not be null!");
this.challenges = challengeTypes.stream().map(String::trim).map(ch -> ch + " realm=\"" + realm + "\"")
.collect(toList());
Expand Down Expand Up @@ -223,6 +237,8 @@ protected void verifyCanAppend(final Set<IRI> modes, final Session session, fina
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
throw new NotAuthorizedException(challenges.get(0),
challenges.subList(1, challenges.size()).toArray());
} else if (hideForbiddenResources) {
throw new NotFoundException();
}
throw new ForbiddenException();
}
Expand All @@ -235,6 +251,8 @@ protected void verifyCanControl(final Set<IRI> modes, final Session session, fin
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
throw new NotAuthorizedException(challenges.get(0),
challenges.subList(1, challenges.size()).toArray());
} else if (hideForbiddenResources) {
throw new NotFoundException();
}
throw new ForbiddenException();
}
Expand All @@ -247,6 +265,8 @@ protected void verifyCanWrite(final Set<IRI> modes, final Session session, final
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
throw new NotAuthorizedException(challenges.get(0),
challenges.subList(1, challenges.size()).toArray());
} else if (hideForbiddenResources) {
throw new NotFoundException();
}
throw new ForbiddenException();
}
Expand All @@ -259,6 +279,8 @@ protected void verifyCanRead(final Set<IRI> modes, final Session session, final
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
throw new NotAuthorizedException(challenges.get(0),
challenges.subList(1, challenges.size()).toArray());
} else if (hideForbiddenResources) {
throw new NotFoundException();
}
throw new ForbiddenException();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static javax.ws.rs.core.Response.Status.OK;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
Expand All @@ -30,6 +31,7 @@

import javax.ws.rs.ForbiddenException;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.Link;
Expand Down Expand Up @@ -126,13 +128,32 @@ public void testFilterRead() throws Exception {

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

@Test
public void testFilterReadHidden() throws Exception {
final Set<IRI> modes = new HashSet<>();
when(mockContext.getMethod()).thenReturn("GET");
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
modes.add(ACL.Read);
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Read ability!");

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

@Test
public void testFilterCustomRead() throws Exception {
final Set<IRI> modes = new HashSet<>();
Expand All @@ -145,7 +166,7 @@ public void testFilterCustomRead() throws Exception {

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
Expand All @@ -165,13 +186,32 @@ public void testFilterWrite() throws Exception {

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

@Test
public void testFilterWriteHidden() throws Exception {
final Set<IRI> modes = new HashSet<>();
when(mockContext.getMethod()).thenReturn("PUT");
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
modes.add(ACL.Write);
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Write ability!");

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

@Test
public void testFilterCustomWrite() throws Exception {
final Set<IRI> modes = new HashSet<>();
Expand All @@ -184,7 +224,7 @@ public void testFilterCustomWrite() throws Exception {

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
Expand All @@ -209,13 +249,32 @@ public void testFilterAppend() throws Exception {

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

@Test
public void testFilterAppendHide() throws Exception {
final Set<IRI> modes = new HashSet<>();
when(mockContext.getMethod()).thenReturn("POST");
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
modes.add(ACL.Append);
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Append ability!");

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

@Test
public void testFilterCustomAppend() throws Exception {
final Set<IRI> modes = new HashSet<>();
Expand All @@ -234,7 +293,7 @@ public void testFilterCustomAppend() throws Exception {

modes.clear();
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
Expand All @@ -255,7 +314,7 @@ public void testFilterControl() throws Exception {
.thenReturn("return=representation; include=\"" + Trellis.PreferAudit.getIRIString() + "\"");

assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

modes.add(ACL.Control);
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Control ability!");
Expand All @@ -272,21 +331,21 @@ public void testFilterControl2() throws Exception {
when(mockContext.getMethod()).thenReturn("GET");
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);

final WebAcFilter filter = new WebAcFilter(mockWebAcService);
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
modes.add(ACL.Read);
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Read ability!");

when(mockQueryParams.getOrDefault(eq("ext"), eq(emptyList()))).thenReturn(asList("acl"));

assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
"No expception thrown when not authorized!");
"No exception thrown when not authorized!");

modes.add(ACL.Control);
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Control ability!");

modes.clear();
when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
"No exception thrown!");
}

Expand All @@ -295,7 +354,7 @@ public void testFilterChallenges() throws Exception {
when(mockContext.getMethod()).thenReturn("POST");
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(emptySet());

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm",
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", false,
"http://example.com/");

final List<Object> challenges = assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
Expand All @@ -312,7 +371,7 @@ public void testFilterResponse() throws Exception {
when(mockResponseContext.getHeaders()).thenReturn(headers);
when(mockUriInfo.getAbsolutePathBuilder()).thenReturn(UriBuilder.fromUri("http://localhost/"));

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", null);
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", false, null);

assertTrue(headers.isEmpty());
filter.filter(mockContext, mockResponseContext);
Expand All @@ -330,7 +389,7 @@ public void testFilterResponseBaseUrl() throws Exception {
when(mockResponseContext.getStatusInfo()).thenReturn(OK);
when(mockResponseContext.getHeaders()).thenReturn(headers);

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm",
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", false,
"http://example.com/");

assertTrue(headers.isEmpty());
Expand All @@ -354,7 +413,7 @@ public void testFilterResponseWebac2() throws Exception {
when(mockUriInfo.getQueryParameters()).thenReturn(params);
when(mockUriInfo.getAbsolutePathBuilder()).thenReturn(UriBuilder.fromUri("http://localhost/"));

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", null);
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", false, null);

assertTrue(headers.isEmpty());
filter.filter(mockContext, mockResponseContext);
Expand All @@ -367,7 +426,20 @@ public void testFilterResponseForbidden() throws Exception {
when(mockResponseContext.getStatusInfo()).thenReturn(FORBIDDEN);
when(mockResponseContext.getHeaders()).thenReturn(headers);

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", null);
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", false, null);

assertTrue(headers.isEmpty());
filter.filter(mockContext, mockResponseContext);
assertTrue(headers.isEmpty());
}

@Test
public void testFilterResponseHidden() throws Exception {
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
when(mockResponseContext.getStatusInfo()).thenReturn(NOT_FOUND);
when(mockResponseContext.getHeaders()).thenReturn(headers);

final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", true, null);

assertTrue(headers.isEmpty());
filter.filter(mockContext, mockResponseContext);
Expand Down

0 comments on commit 15cd821

Please sign in to comment.