Skip to content

Commit b97af66

Browse files
committed
Make forbidden resource response code configurable
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.
1 parent b2d115c commit b97af66

File tree

5 files changed

+131
-18
lines changed

5 files changed

+131
-18
lines changed

components/app/src/main/java/org/trellisldp/app/AbstractTrellisApplication.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public void run(final T config, final Environment environment) throws Exception
142142
of(config.getAuth().getBasic()).filter(BasicAuthConfiguration::getEnabled).map(x -> "Basic")
143143
.ifPresent(challenges::add);
144144
environment.jersey().register(new WebAcFilter(webac, challenges, config.getAuth().getRealm(),
145-
config.getBaseUrl()));
145+
config.getAuth().getHideForbiddenResources(), config.getBaseUrl()));
146146
});
147147

148148
// WebSub

components/app/src/main/java/org/trellisldp/app/config/AuthConfiguration.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class AuthConfiguration {
2727
private JwtAuthConfiguration jwt = new JwtAuthConfiguration();
2828
private BasicAuthConfiguration basic = new BasicAuthConfiguration();
2929
private WebacConfiguration webac = new WebacConfiguration();
30+
private boolean hideForbiddenResources = false;
3031

3132
@NotNull
3233
private String realm = "trellis";
@@ -123,4 +124,22 @@ public String getRealm() {
123124
public void setRealm(final String realm) {
124125
this.realm = realm;
125126
}
127+
128+
/**
129+
* Get whether to hide forbidden resources.
130+
* @return whether to hide forbidden resources; by default, this is false
131+
*/
132+
@JsonProperty
133+
public boolean getHideForbiddenResources() {
134+
return hideForbiddenResources;
135+
}
136+
137+
/**
138+
* Set whether to hide forbidden resources.
139+
* @param hideForbiddenResources true to hide forbidden resources; false otherwise
140+
*/
141+
@JsonProperty
142+
public void setHideForbiddenResources(final boolean hideForbiddenResources) {
143+
this.hideForbiddenResources = hideForbiddenResources;
144+
}
126145
}

components/webac/src/main/java/org/trellisldp/webac/WebAcFilter.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import javax.inject.Inject;
4040
import javax.ws.rs.ForbiddenException;
4141
import javax.ws.rs.NotAuthorizedException;
42+
import javax.ws.rs.NotFoundException;
4243
import javax.ws.rs.container.ContainerRequestContext;
4344
import javax.ws.rs.container.ContainerRequestFilter;
4445
import javax.ws.rs.container.ContainerResponseContext;
@@ -100,6 +101,13 @@ public class WebAcFilter implements ContainerRequestFilter, ContainerResponseFil
100101
/** The configuration key controlling the realm used in a WWW-Authenticate header, or 'trellis' by default. **/
101102
public static final String CONFIG_WEBAC_REALM = "trellis.webac.realm";
102103

104+
/**
105+
* The configuration key controlling the response code for forbidden resources.
106+
*
107+
* <p>A true value will cause 404 Not Found responses to be generated for forbidden resources.
108+
*/
109+
public static final String CONFIG_WEBAC_HIDE_FORBIDDEN_RESOURCES = "trellis.webac.hide.forbidden.resources";
110+
103111
private static final Logger LOGGER = getLogger(WebAcFilter.class);
104112
private static final RDF rdf = getInstance();
105113
private static final String ORIGIN = "Origin";
@@ -110,6 +118,7 @@ public class WebAcFilter implements ContainerRequestFilter, ContainerResponseFil
110118
protected final WebAcService accessService;
111119
private final List<String> challenges;
112120
private final String baseUrl;
121+
private final boolean hideForbiddenResources;
113122

114123
/**
115124
* No-op constructor for CDI.
@@ -118,6 +127,7 @@ public class WebAcFilter implements ContainerRequestFilter, ContainerResponseFil
118127
this.accessService = null;
119128
this.challenges = null;
120129
this.baseUrl = null;
130+
this.hideForbiddenResources = false;
121131
}
122132

123133
/**
@@ -134,6 +144,7 @@ private WebAcFilter(final WebAcService accessService, final Config config) {
134144
this(accessService,
135145
asList(config.getOptionalValue(CONFIG_WEBAC_CHALLENGES, String.class).orElse("").split(",")),
136146
config.getOptionalValue(CONFIG_WEBAC_REALM, String.class).orElse("trellis"),
147+
config.getOptionalValue(CONFIG_WEBAC_HIDE_FORBIDDEN_RESOURCES, Boolean.class).orElse(false),
137148
config.getOptionalValue(CONFIG_HTTP_BASE_URL, String.class).orElse(null));
138149
}
139150

@@ -143,12 +154,15 @@ private WebAcFilter(final WebAcService accessService, final Config config) {
143154
* @param accessService the access service
144155
* @param challengeTypes the WWW-Authenticate challenge types
145156
* @param realm the authentication realm
157+
* @param hideForbiddenResources true indicates using a 404 response code for forbidden resources, thereby
158+
* hiding them from clients; otherwise, 403 response code will be generated for forbidden resources
146159
* @param baseUrl the base URL, may be null
147160
*/
148161
public WebAcFilter(final WebAcService accessService, final List<String> challengeTypes,
149-
final String realm, final String baseUrl) {
162+
final String realm, final boolean hideForbiddenResources, final String baseUrl) {
150163
requireNonNull(challengeTypes, "Challenges may not be null!");
151164
requireNonNull(realm, "Realm may not be null!");
165+
this.hideForbiddenResources = hideForbiddenResources;
152166
this.accessService = requireNonNull(accessService, "Access Control service may not be null!");
153167
this.challenges = challengeTypes.stream().map(String::trim).map(ch -> ch + " realm=\"" + realm + "\"")
154168
.collect(toList());
@@ -223,6 +237,8 @@ protected void verifyCanAppend(final Set<IRI> modes, final Session session, fina
223237
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
224238
throw new NotAuthorizedException(challenges.get(0),
225239
challenges.subList(1, challenges.size()).toArray());
240+
} else if (hideForbiddenResources) {
241+
throw new NotFoundException();
226242
}
227243
throw new ForbiddenException();
228244
}
@@ -235,6 +251,8 @@ protected void verifyCanControl(final Set<IRI> modes, final Session session, fin
235251
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
236252
throw new NotAuthorizedException(challenges.get(0),
237253
challenges.subList(1, challenges.size()).toArray());
254+
} else if (hideForbiddenResources) {
255+
throw new NotFoundException();
238256
}
239257
throw new ForbiddenException();
240258
}
@@ -247,6 +265,8 @@ protected void verifyCanWrite(final Set<IRI> modes, final Session session, final
247265
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
248266
throw new NotAuthorizedException(challenges.get(0),
249267
challenges.subList(1, challenges.size()).toArray());
268+
} else if (hideForbiddenResources) {
269+
throw new NotFoundException();
250270
}
251271
throw new ForbiddenException();
252272
}
@@ -259,6 +279,8 @@ protected void verifyCanRead(final Set<IRI> modes, final Session session, final
259279
if (Trellis.AnonymousAgent.equals(session.getAgent())) {
260280
throw new NotAuthorizedException(challenges.get(0),
261281
challenges.subList(1, challenges.size()).toArray());
282+
} else if (hideForbiddenResources) {
283+
throw new NotFoundException();
262284
}
263285
throw new ForbiddenException();
264286
}

components/webac/src/test/java/org/trellisldp/webac/WebAcFilterTest.java

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static java.util.Collections.emptyList;
1818
import static java.util.Collections.emptySet;
1919
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
20+
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
2021
import static javax.ws.rs.core.Response.Status.OK;
2122
import static org.junit.jupiter.api.Assertions.*;
2223
import static org.mockito.ArgumentMatchers.*;
@@ -30,6 +31,7 @@
3031

3132
import javax.ws.rs.ForbiddenException;
3233
import javax.ws.rs.NotAuthorizedException;
34+
import javax.ws.rs.NotFoundException;
3335
import javax.ws.rs.container.ContainerRequestContext;
3436
import javax.ws.rs.container.ContainerResponseContext;
3537
import javax.ws.rs.core.Link;
@@ -126,13 +128,32 @@ public void testFilterRead() throws Exception {
126128

127129
modes.clear();
128130
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
129-
"No expception thrown when not authorized!");
131+
"No exception thrown when not authorized!");
130132

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

138+
@Test
139+
public void testFilterReadHidden() throws Exception {
140+
final Set<IRI> modes = new HashSet<>();
141+
when(mockContext.getMethod()).thenReturn("GET");
142+
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);
143+
144+
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
145+
modes.add(ACL.Read);
146+
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Read ability!");
147+
148+
modes.clear();
149+
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
150+
"No exception thrown when not authorized!");
151+
152+
when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
153+
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
154+
"No exception thrown!");
155+
}
156+
136157
@Test
137158
public void testFilterCustomRead() throws Exception {
138159
final Set<IRI> modes = new HashSet<>();
@@ -145,7 +166,7 @@ public void testFilterCustomRead() throws Exception {
145166

146167
modes.clear();
147168
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
148-
"No expception thrown when not authorized!");
169+
"No exception thrown when not authorized!");
149170

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

166187
modes.clear();
167188
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
168-
"No expception thrown when not authorized!");
189+
"No exception thrown when not authorized!");
169190

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

196+
@Test
197+
public void testFilterWriteHidden() throws Exception {
198+
final Set<IRI> modes = new HashSet<>();
199+
when(mockContext.getMethod()).thenReturn("PUT");
200+
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);
201+
202+
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
203+
modes.add(ACL.Write);
204+
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Write ability!");
205+
206+
modes.clear();
207+
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
208+
"No exception thrown when not authorized!");
209+
210+
when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
211+
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
212+
"No exception thrown!");
213+
}
214+
175215
@Test
176216
public void testFilterCustomWrite() throws Exception {
177217
final Set<IRI> modes = new HashSet<>();
@@ -184,7 +224,7 @@ public void testFilterCustomWrite() throws Exception {
184224

185225
modes.clear();
186226
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
187-
"No expception thrown when not authorized!");
227+
"No exception thrown when not authorized!");
188228

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

210250
modes.clear();
211251
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
212-
"No expception thrown when not authorized!");
252+
"No exception thrown when not authorized!");
213253

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

259+
@Test
260+
public void testFilterAppendHide() throws Exception {
261+
final Set<IRI> modes = new HashSet<>();
262+
when(mockContext.getMethod()).thenReturn("POST");
263+
when(mockWebAcService.getAccessModes(any(IRI.class), any(Session.class), any())).thenReturn(modes);
264+
265+
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Bearer", "Basic"), "trellis", true, null);
266+
modes.add(ACL.Append);
267+
assertDoesNotThrow(() -> filter.filter(mockContext), "Unexpected exception after adding Append ability!");
268+
269+
modes.clear();
270+
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
271+
"No exception thrown when not authorized!");
272+
273+
when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
274+
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
275+
"No exception thrown!");
276+
}
277+
219278
@Test
220279
public void testFilterCustomAppend() throws Exception {
221280
final Set<IRI> modes = new HashSet<>();
@@ -234,7 +293,7 @@ public void testFilterCustomAppend() throws Exception {
234293

235294
modes.clear();
236295
assertThrows(NotAuthorizedException.class, () -> filter.filter(mockContext),
237-
"No expception thrown when not authorized!");
296+
"No exception thrown when not authorized!");
238297

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

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

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

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

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

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

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

287346
modes.clear();
288347
when(mockContext.getProperty(SESSION_PROPERTY)).thenReturn(session);
289-
assertThrows(ForbiddenException.class, () -> filter.filter(mockContext),
348+
assertThrows(NotFoundException.class, () -> filter.filter(mockContext),
290349
"No exception thrown!");
291350
}
292351

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

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

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

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

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

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

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

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

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

370-
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", null);
429+
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", false, null);
430+
431+
assertTrue(headers.isEmpty());
432+
filter.filter(mockContext, mockResponseContext);
433+
assertTrue(headers.isEmpty());
434+
}
435+
436+
@Test
437+
public void testFilterResponseHidden() throws Exception {
438+
final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
439+
when(mockResponseContext.getStatusInfo()).thenReturn(NOT_FOUND);
440+
when(mockResponseContext.getHeaders()).thenReturn(headers);
441+
442+
final WebAcFilter filter = new WebAcFilter(mockWebAcService, asList("Foo", "Bar"), "my-realm", true, null);
371443

372444
assertTrue(headers.isEmpty());
373445
filter.filter(mockContext, mockResponseContext);

platform/webapp/src/main/java/org/trellisldp/webapp/TrellisApplication.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public TrellisApplication() {
5555
register(new OAuthFilter());
5656
register(new BasicAuthFilter());
5757
register(new WebAcFilter(new WebAcService(serviceBundler.getResourceService()), asList("Basic", "Bearer"),
58-
"trellis", baseUrl));
58+
"trellis", false, baseUrl));
5959

6060
AppUtils.getCacheControlFilter().ifPresent(this::register);
6161
AppUtils.getCORSFilter().ifPresent(this::register);

0 commit comments

Comments
 (0)